@vrplatform/email 1.0.2-38239023185 → 1.0.2-38242168300
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/build/main/components/ActionEmailTemplate.d.ts +1 -1
- package/build/main/components/ActionEmailTemplate.js +2 -2
- package/build/main/components/ActionEmailTemplate.js.map +1 -1
- package/build/main/components/EmailContainer.d.ts +1 -0
- package/build/main/components/EmailContainer.js +2 -2
- package/build/main/components/EmailContainer.js.map +1 -1
- package/build/main/templates/OwnerStatementEmailTemplate.d.ts +7 -2
- package/build/main/templates/OwnerStatementEmailTemplate.js +85 -3
- package/build/main/templates/OwnerStatementEmailTemplate.js.map +1 -1
- package/build/main/types.d.ts +2 -0
- package/build/main/types.js.map +1 -1
- package/build/main/utils/send-email.d.ts +1 -0
- package/build/main/utils/send-email.js +6 -0
- package/build/main/utils/send-email.js.map +1 -1
- package/build/module/components/ActionEmailTemplate.d.ts +1 -1
- package/build/module/components/ActionEmailTemplate.js +2 -2
- package/build/module/components/ActionEmailTemplate.js.map +1 -1
- package/build/module/components/EmailContainer.d.ts +1 -0
- package/build/module/components/EmailContainer.js +2 -2
- package/build/module/components/EmailContainer.js.map +1 -1
- package/build/module/templates/OwnerStatementEmailTemplate.d.ts +7 -2
- package/build/module/templates/OwnerStatementEmailTemplate.js +86 -4
- package/build/module/templates/OwnerStatementEmailTemplate.js.map +1 -1
- package/build/module/types.d.ts +2 -0
- package/build/module/types.js.map +1 -1
- package/build/module/utils/send-email.d.ts +1 -0
- package/build/module/utils/send-email.js +6 -0
- package/build/module/utils/send-email.js.map +1 -1
- package/package.json +1 -1
- package/src/components/ActionEmailTemplate.tsx +2 -0
- package/src/components/EmailContainer.tsx +9 -0
- package/src/templates/OwnerStatementEmailTemplate.tsx +203 -8
- package/src/types.ts +2 -0
- package/src/utils/send-email.ts +7 -0
|
@@ -12,4 +12,4 @@ export type ActionEmailData = {
|
|
|
12
12
|
export interface ActionEmailTemplateProps extends EmailBaseProps {
|
|
13
13
|
data: ActionEmailData;
|
|
14
14
|
}
|
|
15
|
-
export declare const ActionEmailTemplate: ({ data, tenant, themeColors, mode, user, listing, month, }: ActionEmailTemplateProps) => import("react").JSX.Element;
|
|
15
|
+
export declare const ActionEmailTemplate: ({ data, tenant, themeColors, mode, user, listing, month, unsubscribeHref, }: ActionEmailTemplateProps) => import("react").JSX.Element;
|
|
@@ -7,8 +7,8 @@ const text_1 = require("@react-email/text");
|
|
|
7
7
|
const EmailContainer_1 = require("./EmailContainer");
|
|
8
8
|
const utils_1 = require("./utils");
|
|
9
9
|
const Button_1 = require("./Button");
|
|
10
|
-
const ActionEmailTemplate = ({ data, tenant, themeColors, mode, user, listing, month, }) => {
|
|
11
|
-
return ((0, jsx_runtime_1.jsxs)(EmailContainer_1.EmailContainer, { title: data.subject, shortDescription: data.shortDescription, tenant: tenant, mode: mode, children: [(0, jsx_runtime_1.jsx)(heading_1.Heading, { style: {
|
|
10
|
+
const ActionEmailTemplate = ({ data, tenant, themeColors, mode, user, listing, month, unsubscribeHref, }) => {
|
|
11
|
+
return ((0, jsx_runtime_1.jsxs)(EmailContainer_1.EmailContainer, { title: data.subject, shortDescription: data.shortDescription, tenant: tenant, mode: mode, unsubscribeHref: unsubscribeHref, children: [(0, jsx_runtime_1.jsx)(heading_1.Heading, { style: {
|
|
12
12
|
fontSize: 28,
|
|
13
13
|
fontWeight: 600,
|
|
14
14
|
marginBottom: '50px',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ActionEmailTemplate.js","sourceRoot":"src/","sources":["components/ActionEmailTemplate.tsx"],"names":[],"mappings":";;;;AAAA,kDAA+C;AAC/C,4CAAyC;AACzC,qDAAkD;AAElD,mCAAyC;AACzC,qCAAkC;AAiB3B,MAAM,mBAAmB,GAAG,CAAC,EAClC,IAAI,EACJ,MAAM,EACN,WAAW,EACX,IAAI,EACJ,IAAI,EACJ,OAAO,EACP,KAAK,
|
|
1
|
+
{"version":3,"file":"ActionEmailTemplate.js","sourceRoot":"src/","sources":["components/ActionEmailTemplate.tsx"],"names":[],"mappings":";;;;AAAA,kDAA+C;AAC/C,4CAAyC;AACzC,qDAAkD;AAElD,mCAAyC;AACzC,qCAAkC;AAiB3B,MAAM,mBAAmB,GAAG,CAAC,EAClC,IAAI,EACJ,MAAM,EACN,WAAW,EACX,IAAI,EACJ,IAAI,EACJ,OAAO,EACP,KAAK,EACL,eAAe,GACU,EAAE,EAAE;IAC7B,OAAO,CACL,wBAAC,+BAAc,IACb,KAAK,EAAE,IAAI,CAAC,OAAO,EACnB,gBAAgB,EAAE,IAAI,CAAC,gBAAgB,EACvC,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,IAAI,EACV,eAAe,EAAE,eAAe,aAEhC,uBAAC,iBAAO,IACN,KAAK,EAAE;oBACL,QAAQ,EAAE,EAAE;oBACZ,UAAU,EAAE,GAAG;oBACf,YAAY,EAAE,MAAM;iBACrB,YAEA,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,cAAc,MAAM,CAAC,IAAI,GAAG,GAC5C,EAET,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,CACtB,uBAAC,WAAI,IACH,KAAK,EAAE;oBACL,OAAO,EAAE,MAAM;oBACf,eAAe,EAAE,SAAS;oBAC1B,YAAY,EAAE,KAAK;oBACnB,YAAY,EAAE,MAAM;iBACrB,YAEA,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,GACd,CACR,EAED;gBACE,sEAAsE;gBACtE,uBAAuB,EAAE;oBACvB,MAAM,EAAE,IAAA,sBAAc,EAAC,IAAI,CAAC,OAAO,EAAE;wBACnC,IAAI,EAAE,IAAI,CAAC,IAAI;wBACf,QAAQ,EAAE,IAAI,CAAC,SAAS,IAAI,EAAE;wBAC9B,IAAI,EAAE,KAAK,IAAI,EAAE;wBACjB,WAAW,EAAE,OAAO,IAAI,EAAE;qBAC3B,CAAC;iBACH,GACD,EAEF,uBAAC,eAAM,IACL,IAAI,EAAE,IAAI,CAAC,IAAI,EACf,IAAI,EAAE,IAAI,iBACE,sBAAsB,EAClC,KAAK,EAAE,IAAI,CAAC,UAAU,EAAE,IAAI,EAAE,IAAI,mBAAmB,EACrD,WAAW,EAAE,WAAW,GACxB,EAEF;gBACE,sEAAsE;gBACtE,uBAAuB,EAAE;oBACvB,MAAM,EAAE,IAAA,sBAAc,EAAC,IAAI,CAAC,OAAO,EAAE;wBACnC,IAAI,EAAE,IAAI,CAAC,IAAI;wBACf,QAAQ,EAAE,IAAI,CAAC,SAAS,IAAI,EAAE;wBAC9B,IAAI,EAAE,KAAK,IAAI,EAAE;wBACjB,WAAW,EAAE,OAAO,IAAI,EAAE;qBAC3B,CAAC;iBACH,GACD,IACa,CAClB,CAAC;AACJ,CAAC,CAAC;AA1EW,QAAA,mBAAmB,uBA0E9B","sourcesContent":["import { Heading } from '@react-email/heading';\nimport { Text } from '@react-email/text';\nimport { EmailContainer } from './EmailContainer';\nimport type { EmailBaseProps } from '../types';\nimport { parseEmailHtml } from './utils';\nimport { Button } from './Button';\n\nexport type ActionEmailData = {\n subject: string;\n shortDescription: string;\n banner?: string;\n header: string;\n bodyOne: string;\n bodyTwo: string;\n href: string;\n buttonText: string;\n};\n\nexport interface ActionEmailTemplateProps extends EmailBaseProps {\n data: ActionEmailData;\n}\n\nexport const ActionEmailTemplate = ({\n data,\n tenant,\n themeColors,\n mode,\n user,\n listing,\n month,\n unsubscribeHref,\n}: ActionEmailTemplateProps) => {\n return (\n <EmailContainer\n title={data.subject}\n shortDescription={data.shortDescription}\n tenant={tenant}\n mode={mode}\n unsubscribeHref={unsubscribeHref}\n >\n <Heading\n style={{\n fontSize: 28,\n fontWeight: 600,\n marginBottom: '50px',\n }}\n >\n {data.header?.trim() || `Welcome to ${tenant.name}!`}\n </Heading>\n\n {data.banner?.trim() && (\n <Text\n style={{\n padding: '24px',\n backgroundColor: '#f2f3f3',\n borderRadius: '4px',\n marginBottom: '40px',\n }}\n >\n {data.banner.trim()}\n </Text>\n )}\n\n <div\n // biome-ignore lint/security/noDangerouslySetInnerHtml: <explanation>\n dangerouslySetInnerHTML={{\n __html: parseEmailHtml(data.bodyOne, {\n href: data.href,\n userName: user.firstName || '',\n date: month || '',\n listingName: listing || '',\n }),\n }}\n />\n\n <Button\n href={data.href}\n mode={mode}\n data-testid=\"accept-invite-button\"\n label={data.buttonText?.trim() || 'Accept Invitation'}\n themeColors={themeColors}\n />\n\n <div\n // biome-ignore lint/security/noDangerouslySetInnerHtml: <explanation>\n dangerouslySetInnerHTML={{\n __html: parseEmailHtml(data.bodyTwo, {\n href: data.href,\n userName: user.firstName || '',\n date: month || '',\n listingName: listing || '',\n }),\n }}\n />\n </EmailContainer>\n );\n};\n"]}
|
|
@@ -31,7 +31,7 @@ const EmailContainer = (props) => {
|
|
|
31
31
|
}, fontWeight: 600, fontStyle: "normal" })] }), shortDescription && (0, jsx_runtime_1.jsx)(preview_1.Preview, { children: shortDescription }), (0, jsx_runtime_1.jsx)(body_1.Body, { style: contentWrapperStyle, children: (0, jsx_runtime_1.jsx)(Content, { ...props, children: children }) })] }));
|
|
32
32
|
};
|
|
33
33
|
exports.EmailContainer = EmailContainer;
|
|
34
|
-
const Content = ({ children, tenant: { logo: tenantLogo, name, showPlatformBranding = true }, mode, }) => {
|
|
34
|
+
const Content = ({ children, tenant: { logo: tenantLogo, name, showPlatformBranding = true }, mode, unsubscribeHref, }) => {
|
|
35
35
|
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [tenantLogo ? ((0, jsx_runtime_1.jsx)("div", { style: {
|
|
36
36
|
marginBottom: '60px',
|
|
37
37
|
padding: '5px',
|
|
@@ -43,7 +43,7 @@ const Content = ({ children, tenant: { logo: tenantLogo, name, showPlatformBrand
|
|
|
43
43
|
alignItems: 'center',
|
|
44
44
|
}, children: (0, jsx_runtime_1.jsx)(img_1.Img, { src: tenantLogo, alt: 'Logo', width: 70 }) })) : !showPlatformBranding ? ((0, jsx_runtime_1.jsx)(text_1.Text, { children: name })) : mode === 'send' ? ((0, jsx_runtime_1.jsx)(img_1.Img, { src: 'https://staging.portal.vrplatform.app/logo.png', alt: 'Logo', width: 70, style: { marginBottom: '60px' } })) : ((0, jsx_runtime_1.jsx)("div", { style: {
|
|
45
45
|
width: '60px',
|
|
46
|
-
}, children: (0, jsx_runtime_1.jsx)(VrpLogo, {}) })), children, showPlatformBranding ? (0, jsx_runtime_1.jsx)(Footer, {}) : (0, jsx_runtime_1.jsx)(text_1.Text, { children: name })] }));
|
|
46
|
+
}, children: (0, jsx_runtime_1.jsx)(VrpLogo, {}) })), children, showPlatformBranding ? (0, jsx_runtime_1.jsx)(Footer, {}) : (0, jsx_runtime_1.jsx)(text_1.Text, { children: name }), unsubscribeHref && ((0, jsx_runtime_1.jsx)(text_1.Text, { style: { color: '#737373' }, children: (0, jsx_runtime_1.jsx)("a", { href: unsubscribeHref, style: { color: '#737373' }, children: "Unsubscribe from owner statement emails" }) }))] }));
|
|
47
47
|
};
|
|
48
48
|
const Footer = () => ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(hr_1.Hr, { style: {
|
|
49
49
|
marginTop: '30px',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EmailContainer.js","sourceRoot":"src/","sources":["components/EmailContainer.tsx"],"names":[],"mappings":";;;;AAAA,4CAAyC;AACzC,4CAAyC;AACzC,4CAAyC;AACzC,wCAAqC;AACrC,4CAAyC;AACzC,0CAAuC;AACvC,kDAA+C;AAC/C,4CAAyC;AACzC,6CAAwC;
|
|
1
|
+
{"version":3,"file":"EmailContainer.js","sourceRoot":"src/","sources":["components/EmailContainer.tsx"],"names":[],"mappings":"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","sourcesContent":["import { Body } from '@react-email/body';\nimport { Font } from '@react-email/font';\nimport { Head } from '@react-email/head';\nimport { Hr } from '@react-email/hr';\nimport { Html } from '@react-email/html';\nimport { Img } from '@react-email/img';\nimport { Preview } from '@react-email/preview';\nimport { Text } from '@react-email/text';\nimport { day } from '@vrplatform/utils';\nimport type { EmailBaseProps } from '../types';\n\ntype Props = {\n children: React.ReactNode;\n title: string;\n tenant: Omit<EmailBaseProps['tenant'], 'name'> & { name?: string };\n mode: EmailBaseProps['mode'];\n shortDescription?: string;\n unsubscribeHref?: string;\n};\n\nexport const EmailContainer = (props: Props) => {\n const { children, shortDescription, title } = props;\n\n const contentWrapperStyle = {\n paddingInline: '20px',\n paddingBlock: '10px',\n maxWidth: '500px',\n margin: '0 auto',\n };\n\n if (props.mode !== 'send') {\n return (\n <div style={contentWrapperStyle}>\n <Content {...props}>{children}</Content>\n </div>\n );\n }\n\n return (\n <Html lang=\"en\">\n <Head>\n <title>{title}</title>\n <Font\n fontFamily=\"Inter\"\n fallbackFontFamily=\"Verdana\"\n webFont={{\n url: 'https://fonts.gstatic.com/s/inter/v13/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7W0Q5nw.woff2',\n format: 'woff2',\n }}\n fontWeight={400}\n fontStyle=\"normal\"\n />\n <Font\n fontFamily=\"Inter\"\n fallbackFontFamily=\"Verdana\"\n webFont={{\n url: 'https://fonts.gstatic.com/s/inter/v13/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7W0Q5nw.woff2',\n format: 'woff2',\n }}\n fontWeight={600}\n fontStyle=\"normal\"\n />\n </Head>\n {shortDescription && <Preview>{shortDescription}</Preview>}\n <Body style={contentWrapperStyle}>\n <Content {...props}>{children}</Content>\n </Body>\n </Html>\n );\n};\n\nconst Content = ({\n children,\n tenant: { logo: tenantLogo, name, showPlatformBranding = true },\n mode,\n unsubscribeHref,\n}: Props) => {\n return (\n <>\n {tenantLogo ? (\n <div\n style={{\n marginBottom: '60px',\n padding: '5px',\n // border: '1px solid rgb(233,236,239)',\n borderRadius: '5px',\n width: '80px',\n display: 'flex',\n justifyContent: 'center',\n alignItems: 'center',\n }}\n >\n <Img src={tenantLogo} alt={'Logo'} width={70} />\n </div>\n ) : !showPlatformBranding ? (\n <Text>{name}</Text>\n ) : mode === 'send' ? (\n <Img\n src={'https://staging.portal.vrplatform.app/logo.png'}\n alt={'Logo'}\n width={70}\n style={{ marginBottom: '60px' }}\n />\n ) : (\n <div\n style={{\n width: '60px',\n }}\n >\n <VrpLogo />\n </div>\n )}\n\n {children}\n\n {showPlatformBranding ? <Footer /> : <Text>{name}</Text>}\n {unsubscribeHref && (\n <Text style={{ color: '#737373' }}>\n <a href={unsubscribeHref} style={{ color: '#737373' }}>\n Unsubscribe from owner statement emails\n </a>\n </Text>\n )}\n </>\n );\n};\n\nconst Footer = () => (\n <>\n <Hr\n style={{\n marginTop: '30px',\n marginBottom: '50px',\n }}\n />\n\n <Text\n style={{\n color: '#737373',\n }}\n >\n © {day().year()} VR Technologies. All rights reserved.\n </Text>\n <Text\n style={{\n color: '#737373',\n }}\n >\n VR Technologies, LLC, a company incorporated and registered in the United\n States of America with company number 20201965158\n </Text>\n </>\n);\n\nconst VrpLogo = () => (\n <svg\n width=\"100%\"\n height=\"100%\"\n viewBox=\"0 0 28 29\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path\n d=\"M18.2549 8.25161C18.2549 5.89704 20.1636 3.98828 22.5182 3.98828C24.8728 3.98828 26.7815 5.89704 26.7815 8.25161C26.7815 10.6062 24.8728 12.5149 22.5182 12.5149C20.1636 12.5149 18.2549 10.6062 18.2549 8.25161Z\"\n fill=\"#172554\"\n />\n <path\n d=\"M7.02441 8.46593C7.02441 6.01867 9.20858 3.99805 11.6558 3.99805C14.1031 3.99805 15.0558 5.59785 16.5612 7.52728C17.1957 8.34054 20.7921 13.3413 20.7921 13.3413C20.7921 13.3413 21.566 14.6863 21.566 15.7894C21.566 18.5287 17.1993 22.8381 17.1993 22.8381L7.66841 10.6799C7.66841 10.6799 7.02441 10.0502 7.02441 8.46593Z\"\n fill=\"#172554\"\n fillOpacity=\"0.8\"\n />\n <path\n d=\"M1.24707 8.42687C1.24707 5.97961 3.43123 3.95898 5.8785 3.95898C8.32576 3.95898 9.27845 5.55879 10.7839 7.48824C11.4184 8.30147 14.6041 12.4888 16.8247 15.5727C18.3585 17.703 18.474 17.705 18.474 20.3439C18.474 22.9827 15.1592 24.1379 14.1483 24.1914C12.0203 24.304 10.5418 22.4681 10.5418 22.4681L1.89106 10.6408C1.89106 10.6408 1.24707 10.0111 1.24707 8.42687Z\"\n fill=\"#172554\"\n />\n </svg>\n);\n"]}
|
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ActionEmailData, ActionEmailTemplateProps } from '../components/ActionEmailTemplate';
|
|
2
2
|
export declare const getDefaultOwnerStatementEmailData: (mode: "tag" | "text", args: {
|
|
3
3
|
appName: string;
|
|
4
4
|
ownerStatementHref: string;
|
|
5
5
|
listingName: string;
|
|
6
6
|
userName: string;
|
|
7
7
|
month: string;
|
|
8
|
+
/** The owner opens the statement through an emailed link, without a login. */
|
|
9
|
+
noLogin?: boolean;
|
|
8
10
|
}) => ActionEmailData;
|
|
9
|
-
export declare const OwnerStatementEmailTemplate: (
|
|
11
|
+
export declare const OwnerStatementEmailTemplate: ({ data, tenant, themeColors, mode, user, listing, month, unsubscribeHref, renewHref, }: ActionEmailTemplateProps & {
|
|
12
|
+
/** Requests a new no-login link once the emailed one has expired. */
|
|
13
|
+
renewHref?: string;
|
|
14
|
+
}) => import("react").JSX.Element;
|
|
@@ -2,13 +2,34 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.OwnerStatementEmailTemplate = exports.getDefaultOwnerStatementEmailData = void 0;
|
|
4
4
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
const heading_1 = require("@react-email/heading");
|
|
6
|
+
const text_1 = require("@react-email/text");
|
|
5
7
|
const extensions_1 = require("../extensions");
|
|
6
|
-
const
|
|
8
|
+
const Button_1 = require("../components/Button");
|
|
9
|
+
const EmailContainer_1 = require("../components/EmailContainer");
|
|
10
|
+
const utils_1 = require("../components/utils");
|
|
7
11
|
const getDefaultOwnerStatementEmailData = (mode, args) => {
|
|
8
12
|
const ownerStatementHref = mode === 'tag' ? extensions_1.EMAIL_ACTION_LINK_COMPONENT : args.ownerStatementHref;
|
|
9
13
|
const listingName = mode === 'tag' ? extensions_1.EMAIL_LISTING_COMPONENT : args.listingName;
|
|
10
14
|
const month = mode === 'tag' ? extensions_1.EMAIL_DATE_COMPONENT : args.month;
|
|
11
15
|
const userName = mode === 'tag' ? extensions_1.EMAIL_USER_NAME_COMPONENT : args.userName;
|
|
16
|
+
if (args.noLogin) {
|
|
17
|
+
// Subject and preview text are plain text without tokens, so a saved
|
|
18
|
+
// template must not keep the editor's sample month and listing.
|
|
19
|
+
return {
|
|
20
|
+
subject: mode === 'tag'
|
|
21
|
+
? 'Your owner statement is ready'
|
|
22
|
+
: `Your ${args.month} owner statement for ${args.listingName}`,
|
|
23
|
+
shortDescription: mode === 'tag'
|
|
24
|
+
? 'Your owner statement is ready. Open it with one click, no login needed.'
|
|
25
|
+
: `Your ${args.month} statement for ${args.listingName} is ready. Open it with one click, no login needed.`,
|
|
26
|
+
header: 'Your owner statement is ready',
|
|
27
|
+
bodyOne: `<p>Hi ${userName},</p><p>${args.appName} published your ${month} owner statement for ${listingName}. Open it with the button below. You don't need a login.</p>`,
|
|
28
|
+
buttonText: 'View statement',
|
|
29
|
+
bodyTwo: `<p style="color:#737373;font-size:13px">If the button doesn't work, copy this link into your browser: ${ownerStatementHref}</p>`,
|
|
30
|
+
href: args.ownerStatementHref,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
12
33
|
return {
|
|
13
34
|
subject: 'Owner statement was published',
|
|
14
35
|
shortDescription: `An owner statement is ready for you. Log in to ${args.appName} now to view it.`,
|
|
@@ -20,8 +41,69 @@ const getDefaultOwnerStatementEmailData = (mode, args) => {
|
|
|
20
41
|
};
|
|
21
42
|
};
|
|
22
43
|
exports.getDefaultOwnerStatementEmailData = getDefaultOwnerStatementEmailData;
|
|
23
|
-
const
|
|
24
|
-
|
|
44
|
+
const mutedColor = '#737373';
|
|
45
|
+
const borderColor = '#e5e7eb';
|
|
46
|
+
const OwnerStatementEmailTemplate = ({ data, tenant, themeColors, mode, user, listing, month, unsubscribeHref, renewHref, }) => {
|
|
47
|
+
const parse = (html) => (0, utils_1.parseEmailHtml)(html, {
|
|
48
|
+
href: data.href,
|
|
49
|
+
userName: user.firstName || '',
|
|
50
|
+
date: month || '',
|
|
51
|
+
listingName: listing || '',
|
|
52
|
+
});
|
|
53
|
+
return ((0, jsx_runtime_1.jsxs)(EmailContainer_1.EmailContainer, { title: data.subject, shortDescription: data.shortDescription, tenant: tenant, mode: mode, unsubscribeHref: unsubscribeHref, children: [(0, jsx_runtime_1.jsx)(text_1.Text, { style: {
|
|
54
|
+
color: themeColors[8],
|
|
55
|
+
fontSize: 12,
|
|
56
|
+
fontWeight: 600,
|
|
57
|
+
letterSpacing: '0.08em',
|
|
58
|
+
textTransform: 'uppercase',
|
|
59
|
+
margin: '0 0 8px',
|
|
60
|
+
}, children: "Owner statement" }), (0, jsx_runtime_1.jsx)(heading_1.Heading, { style: {
|
|
61
|
+
fontSize: 26,
|
|
62
|
+
fontWeight: 600,
|
|
63
|
+
lineHeight: '34px',
|
|
64
|
+
margin: '0 0 32px',
|
|
65
|
+
}, children: data.header?.trim() || 'Your owner statement is ready' }), data.banner?.trim() && ((0, jsx_runtime_1.jsx)(text_1.Text, { style: {
|
|
66
|
+
padding: '16px 20px',
|
|
67
|
+
backgroundColor: '#f8f9fa',
|
|
68
|
+
borderLeft: `3px solid ${themeColors[8]}`,
|
|
69
|
+
borderRadius: '4px',
|
|
70
|
+
margin: '0 0 32px',
|
|
71
|
+
whiteSpace: 'pre-line',
|
|
72
|
+
}, children: data.banner.trim() })), (0, jsx_runtime_1.jsx)("div", {
|
|
73
|
+
// biome-ignore lint/security/noDangerouslySetInnerHtml: values are escaped by parseEmailHtml
|
|
74
|
+
dangerouslySetInnerHTML: { __html: parse(data.bodyOne) } }), (listing || month) && ((0, jsx_runtime_1.jsx)("table", { role: "presentation", cellPadding: 0, cellSpacing: 0, width: "100%", style: {
|
|
75
|
+
border: `1px solid ${borderColor}`,
|
|
76
|
+
borderRadius: '8px',
|
|
77
|
+
borderCollapse: 'separate',
|
|
78
|
+
margin: '24px 0 0',
|
|
79
|
+
}, children: (0, jsx_runtime_1.jsx)("tbody", { children: [
|
|
80
|
+
['Property', listing],
|
|
81
|
+
['Statement', month],
|
|
82
|
+
].map(([label, value], index) => value ? ((0, jsx_runtime_1.jsxs)("tr", { children: [(0, jsx_runtime_1.jsx)("td", { style: {
|
|
83
|
+
padding: '14px 20px',
|
|
84
|
+
color: mutedColor,
|
|
85
|
+
fontSize: 14,
|
|
86
|
+
borderTop: index ? `1px solid ${borderColor}` : 'none',
|
|
87
|
+
}, children: label }), (0, jsx_runtime_1.jsx)("td", { style: {
|
|
88
|
+
padding: '14px 20px',
|
|
89
|
+
fontSize: 14,
|
|
90
|
+
fontWeight: 600,
|
|
91
|
+
textAlign: 'right',
|
|
92
|
+
borderTop: index ? `1px solid ${borderColor}` : 'none',
|
|
93
|
+
}, children: value })] }, label)) : null) }) })), (0, jsx_runtime_1.jsx)(Button_1.Button, { href: data.href, mode: mode, "data-testid": "view-owner-statement-button", label: data.buttonText?.trim() || 'View statement', themeColors: themeColors, style: { marginTop: '24px', marginBottom: '32px' } }), renewHref && ((0, jsx_runtime_1.jsxs)("div", { style: {
|
|
94
|
+
padding: '20px',
|
|
95
|
+
backgroundColor: '#f8f9fa',
|
|
96
|
+
borderRadius: '8px',
|
|
97
|
+
marginBottom: '32px',
|
|
98
|
+
}, children: [(0, jsx_runtime_1.jsx)(text_1.Text, { style: { fontWeight: 600, margin: '0 0 4px' }, children: "Link expired?" }), (0, jsx_runtime_1.jsx)(text_1.Text, { style: { color: mutedColor, fontSize: 14, margin: 0 }, children: "For your security, this link opens only this statement and works for 7 days. After that, request a new one and we'll email it to this address." }), (0, jsx_runtime_1.jsx)(Button_1.Button, { href: renewHref, mode: mode, "data-testid": "renew-owner-statement-link-button", label: "Send me a new link", themeColors: themeColors, style: {
|
|
99
|
+
background: '#ffffff',
|
|
100
|
+
border: `1px solid ${themeColors[8]}`,
|
|
101
|
+
color: themeColors[8],
|
|
102
|
+
marginTop: '16px',
|
|
103
|
+
marginBottom: 0,
|
|
104
|
+
} })] })), (0, jsx_runtime_1.jsx)("div", {
|
|
105
|
+
// biome-ignore lint/security/noDangerouslySetInnerHtml: values are escaped by parseEmailHtml
|
|
106
|
+
dangerouslySetInnerHTML: { __html: parse(data.bodyTwo) } })] }));
|
|
25
107
|
};
|
|
26
108
|
exports.OwnerStatementEmailTemplate = OwnerStatementEmailTemplate;
|
|
27
109
|
//# sourceMappingURL=OwnerStatementEmailTemplate.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OwnerStatementEmailTemplate.js","sourceRoot":"src/","sources":["templates/OwnerStatementEmailTemplate.tsx"],"names":[],"mappings":";;;;AAAA,8CAKuB;AACvB,2EAI2C;AAEpC,MAAM,iCAAiC,GAAG,CAC/C,IAAoB,EACpB,IAMC,EACgB,EAAE;IACnB,MAAM,kBAAkB,GACtB,IAAI,KAAK,KAAK,CAAC,CAAC,CAAC,wCAA2B,CAAC,CAAC,CAAC,IAAI,CAAC,kBAAkB,CAAC;IAEzE,MAAM,WAAW,GACf,IAAI,KAAK,KAAK,CAAC,CAAC,CAAC,oCAAuB,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC;IAC9D,MAAM,KAAK,GAAG,IAAI,KAAK,KAAK,CAAC,CAAC,CAAC,iCAAoB,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC;IACjE,MAAM,QAAQ,GAAG,IAAI,KAAK,KAAK,CAAC,CAAC,CAAC,sCAAyB,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC;IAE5E,OAAO;QACL,OAAO,EAAE,+BAA+B;QACxC,gBAAgB,EAAE,kDAAkD,IAAI,CAAC,OAAO,kBAAkB;QAClG,MAAM,EAAE,+BAA+B;QACvC,OAAO,EAAE,SAAS,QAAQ,YAAY,KAAK,iCAAiC,WAAW,gCAAgC,IAAI,CAAC,OAAO,sEAAsE;QACzM,UAAU,EAAE,sBAAsB;QAClC,OAAO,EAAE,wGAAwG,kBAAkB,MAAM;QACzI,IAAI,EAAE,IAAI,CAAC,kBAAkB;KAC9B,CAAC;AACJ,CAAC,CAAC;AA3BW,QAAA,iCAAiC,qCA2B5C;AAEK,MAAM,2BAA2B,GAAG,CACzC,KAA+B,EAC/B,EAAE;IACF,OAAO,uBAAC,yCAAmB,OAAK,KAAK,GAAI,CAAC;AAC5C,CAAC,CAAC;AAJW,QAAA,2BAA2B,+BAItC","sourcesContent":["import {\n EMAIL_ACTION_LINK_COMPONENT,\n EMAIL_DATE_COMPONENT,\n EMAIL_LISTING_COMPONENT,\n EMAIL_USER_NAME_COMPONENT,\n} from '../extensions';\nimport {\n type ActionEmailData,\n ActionEmailTemplate,\n type ActionEmailTemplateProps,\n} from '../components/ActionEmailTemplate';\n\nexport const getDefaultOwnerStatementEmailData = (\n mode: 'tag' | 'text',\n args: {\n appName: string;\n ownerStatementHref: string;\n listingName: string;\n userName: string;\n month: string;\n }\n): ActionEmailData => {\n const ownerStatementHref =\n mode === 'tag' ? EMAIL_ACTION_LINK_COMPONENT : args.ownerStatementHref;\n\n const listingName =\n mode === 'tag' ? EMAIL_LISTING_COMPONENT : args.listingName;\n const month = mode === 'tag' ? EMAIL_DATE_COMPONENT : args.month;\n const userName = mode === 'tag' ? EMAIL_USER_NAME_COMPONENT : args.userName;\n\n return {\n subject: 'Owner statement was published',\n shortDescription: `An owner statement is ready for you. Log in to ${args.appName} now to view it.`,\n header: 'Owner statement was published',\n bodyOne: `<p>Hi ${userName},<p>Your ${month} owner statement for property ${listingName} is ready for your review on ${args.appName}.</p><p>To access your statement, please click the button below:</p>`,\n buttonText: 'View Owner statement',\n bodyTwo: `<p>If you have any trouble with the button, you can copy and paste the link below into your browser: ${ownerStatementHref}</p>`,\n href: args.ownerStatementHref,\n };\n};\n\nexport const OwnerStatementEmailTemplate = (\n props: ActionEmailTemplateProps\n) => {\n return <ActionEmailTemplate {...props} />;\n};\n"]}
|
|
1
|
+
{"version":3,"file":"OwnerStatementEmailTemplate.js","sourceRoot":"src/","sources":["templates/OwnerStatementEmailTemplate.tsx"],"names":[],"mappings":"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","sourcesContent":["import { Heading } from '@react-email/heading';\nimport { Text } from '@react-email/text';\nimport {\n EMAIL_ACTION_LINK_COMPONENT,\n EMAIL_DATE_COMPONENT,\n EMAIL_LISTING_COMPONENT,\n EMAIL_USER_NAME_COMPONENT,\n} from '../extensions';\nimport type {\n ActionEmailData,\n ActionEmailTemplateProps,\n} from '../components/ActionEmailTemplate';\nimport { Button } from '../components/Button';\nimport { EmailContainer } from '../components/EmailContainer';\nimport { parseEmailHtml } from '../components/utils';\n\nexport const getDefaultOwnerStatementEmailData = (\n mode: 'tag' | 'text',\n args: {\n appName: string;\n ownerStatementHref: string;\n listingName: string;\n userName: string;\n month: string;\n /** The owner opens the statement through an emailed link, without a login. */\n noLogin?: boolean;\n }\n): ActionEmailData => {\n const ownerStatementHref =\n mode === 'tag' ? EMAIL_ACTION_LINK_COMPONENT : args.ownerStatementHref;\n\n const listingName =\n mode === 'tag' ? EMAIL_LISTING_COMPONENT : args.listingName;\n const month = mode === 'tag' ? EMAIL_DATE_COMPONENT : args.month;\n const userName = mode === 'tag' ? EMAIL_USER_NAME_COMPONENT : args.userName;\n\n if (args.noLogin) {\n // Subject and preview text are plain text without tokens, so a saved\n // template must not keep the editor's sample month and listing.\n return {\n subject:\n mode === 'tag'\n ? 'Your owner statement is ready'\n : `Your ${args.month} owner statement for ${args.listingName}`,\n shortDescription:\n mode === 'tag'\n ? 'Your owner statement is ready. Open it with one click, no login needed.'\n : `Your ${args.month} statement for ${args.listingName} is ready. Open it with one click, no login needed.`,\n header: 'Your owner statement is ready',\n bodyOne: `<p>Hi ${userName},</p><p>${args.appName} published your ${month} owner statement for ${listingName}. Open it with the button below. You don't need a login.</p>`,\n buttonText: 'View statement',\n bodyTwo: `<p style=\"color:#737373;font-size:13px\">If the button doesn't work, copy this link into your browser: ${ownerStatementHref}</p>`,\n href: args.ownerStatementHref,\n };\n }\n\n return {\n subject: 'Owner statement was published',\n shortDescription: `An owner statement is ready for you. Log in to ${args.appName} now to view it.`,\n header: 'Owner statement was published',\n bodyOne: `<p>Hi ${userName},<p>Your ${month} owner statement for property ${listingName} is ready for your review on ${args.appName}.</p><p>To access your statement, please click the button below:</p>`,\n buttonText: 'View Owner statement',\n bodyTwo: `<p>If you have any trouble with the button, you can copy and paste the link below into your browser: ${ownerStatementHref}</p>`,\n href: args.ownerStatementHref,\n };\n};\n\nconst mutedColor = '#737373';\nconst borderColor = '#e5e7eb';\n\nexport const OwnerStatementEmailTemplate = ({\n data,\n tenant,\n themeColors,\n mode,\n user,\n listing,\n month,\n unsubscribeHref,\n renewHref,\n}: ActionEmailTemplateProps & {\n /** Requests a new no-login link once the emailed one has expired. */\n renewHref?: string;\n}) => {\n const parse = (html: string) =>\n parseEmailHtml(html, {\n href: data.href,\n userName: user.firstName || '',\n date: month || '',\n listingName: listing || '',\n });\n\n return (\n <EmailContainer\n title={data.subject}\n shortDescription={data.shortDescription}\n tenant={tenant}\n mode={mode}\n unsubscribeHref={unsubscribeHref}\n >\n <Text\n style={{\n color: themeColors[8],\n fontSize: 12,\n fontWeight: 600,\n letterSpacing: '0.08em',\n textTransform: 'uppercase',\n margin: '0 0 8px',\n }}\n >\n Owner statement\n </Text>\n <Heading\n style={{\n fontSize: 26,\n fontWeight: 600,\n lineHeight: '34px',\n margin: '0 0 32px',\n }}\n >\n {data.header?.trim() || 'Your owner statement is ready'}\n </Heading>\n\n {data.banner?.trim() && (\n <Text\n style={{\n padding: '16px 20px',\n backgroundColor: '#f8f9fa',\n borderLeft: `3px solid ${themeColors[8]}`,\n borderRadius: '4px',\n margin: '0 0 32px',\n whiteSpace: 'pre-line',\n }}\n >\n {data.banner.trim()}\n </Text>\n )}\n\n <div\n // biome-ignore lint/security/noDangerouslySetInnerHtml: values are escaped by parseEmailHtml\n dangerouslySetInnerHTML={{ __html: parse(data.bodyOne) }}\n />\n\n {(listing || month) && (\n <table\n role=\"presentation\"\n cellPadding={0}\n cellSpacing={0}\n width=\"100%\"\n style={{\n border: `1px solid ${borderColor}`,\n borderRadius: '8px',\n borderCollapse: 'separate',\n margin: '24px 0 0',\n }}\n >\n <tbody>\n {[\n ['Property', listing],\n ['Statement', month],\n ].map(([label, value], index) =>\n value ? (\n <tr key={label}>\n <td\n style={{\n padding: '14px 20px',\n color: mutedColor,\n fontSize: 14,\n borderTop: index ? `1px solid ${borderColor}` : 'none',\n }}\n >\n {label}\n </td>\n <td\n style={{\n padding: '14px 20px',\n fontSize: 14,\n fontWeight: 600,\n textAlign: 'right',\n borderTop: index ? `1px solid ${borderColor}` : 'none',\n }}\n >\n {value}\n </td>\n </tr>\n ) : null\n )}\n </tbody>\n </table>\n )}\n\n <Button\n href={data.href}\n mode={mode}\n data-testid=\"view-owner-statement-button\"\n label={data.buttonText?.trim() || 'View statement'}\n themeColors={themeColors}\n style={{ marginTop: '24px', marginBottom: '32px' }}\n />\n\n {renewHref && (\n <div\n style={{\n padding: '20px',\n backgroundColor: '#f8f9fa',\n borderRadius: '8px',\n marginBottom: '32px',\n }}\n >\n <Text style={{ fontWeight: 600, margin: '0 0 4px' }}>\n Link expired?\n </Text>\n <Text style={{ color: mutedColor, fontSize: 14, margin: 0 }}>\n For your security, this link opens only this statement and works for\n 7 days. After that, request a new one and we'll email it to this\n address.\n </Text>\n <Button\n href={renewHref}\n mode={mode}\n data-testid=\"renew-owner-statement-link-button\"\n label=\"Send me a new link\"\n themeColors={themeColors}\n style={{\n background: '#ffffff',\n border: `1px solid ${themeColors[8]}`,\n color: themeColors[8],\n marginTop: '16px',\n marginBottom: 0,\n }}\n />\n </div>\n )}\n\n <div\n // biome-ignore lint/security/noDangerouslySetInnerHtml: values are escaped by parseEmailHtml\n dangerouslySetInnerHTML={{ __html: parse(data.bodyTwo) }}\n />\n </EmailContainer>\n );\n};\n"]}
|
package/build/main/types.d.ts
CHANGED
package/build/main/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"src/","sources":["types.ts"],"names":[],"mappings":"","sourcesContent":["export type EmailBaseProps = {\n mode: 'edit' | 'preview' | 'send';\n tenant: {\n name: string;\n showPlatformBranding?: boolean;\n logo: string | undefined | null;\n };\n listing: string | undefined;\n month: string | undefined;\n user: {\n firstName: string | undefined;\n };\n themeColors: readonly [\n string,\n string,\n string,\n string,\n string,\n string,\n string,\n string,\n string,\n string,\n ...string[],\n ];\n};\n"]}
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"src/","sources":["types.ts"],"names":[],"mappings":"","sourcesContent":["export type EmailBaseProps = {\n mode: 'edit' | 'preview' | 'send';\n tenant: {\n name: string;\n showPlatformBranding?: boolean;\n logo: string | undefined | null;\n };\n listing: string | undefined;\n month: string | undefined;\n /** Opt-out link shown below the email body */\n unsubscribeHref?: string;\n user: {\n firstName: string | undefined;\n };\n themeColors: readonly [\n string,\n string,\n string,\n string,\n string,\n string,\n string,\n string,\n string,\n string,\n ...string[],\n ];\n};\n"]}
|
|
@@ -29,6 +29,7 @@ export declare function sendEmail(data: {
|
|
|
29
29
|
tag?: string;
|
|
30
30
|
metadata?: Record<string, string>;
|
|
31
31
|
from?: string;
|
|
32
|
+
headers?: Record<string, string>;
|
|
32
33
|
}, options: {
|
|
33
34
|
fetch?: (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
34
35
|
postmarkToken?: string;
|
|
@@ -65,6 +65,12 @@ async function sendEmail(data, options) {
|
|
|
65
65
|
MessageStream: 'outbound',
|
|
66
66
|
Tag: data.tag,
|
|
67
67
|
Metadata: data.metadata,
|
|
68
|
+
Headers: data.headers
|
|
69
|
+
? Object.entries(data.headers).map(([Name, Value]) => ({
|
|
70
|
+
Name,
|
|
71
|
+
Value,
|
|
72
|
+
}))
|
|
73
|
+
: undefined,
|
|
68
74
|
}),
|
|
69
75
|
});
|
|
70
76
|
const providerResponse = await response.json().catch(() => ({}));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"send-email.js","sourceRoot":"src/","sources":["utils/send-email.ts"],"names":[],"mappings":";;;AAsDA,oDAMC;AAED,8BAwGC;AAED,sDAkEC;AA1OD,6CAAwC;AACxC,6BAAwB;AAExB,MAAM,uBAAuB,GAAG,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC;AAC/D,MAAM,+BAA+B,GAAG,OAAC;KACtC,MAAM,CAAC,EAAE,SAAS,EAAE,uBAAuB,EAAE,CAAC;KAC9C,SAAS,CAAC,CAAC,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC,SAAS,CAAC,CAAC;AAC3C,MAAM,6BAA6B,GAAG,OAAC;KACpC,MAAM,EAAE;KACR,KAAK,CAAC,OAAO,CAAC;KACd,SAAS,CAAC,MAAM,CAAC;KACjB,IAAI,CAAC,uBAAuB,CAAC,CAAC;AACjC,MAAM,6BAA6B,GAAG,OAAC,CAAC,MAAM,CAAC;IAC7C,SAAS,EAAE,OAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IACvB,SAAS,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC5B,WAAW,EAAE,OAAC,CAAC,MAAM,EAAE,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAA,WAAG,EAAC,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC;CAChE,CAAC,CAAC;AAEH,SAAS,aAAa,CAAI,MAAoB,EAAE,KAAc;IAC5D,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IACvC,IAAI,MAAM,CAAC,OAAO;QAAE,OAAO,MAAM,CAAC,IAAI,CAAC;AACzC,CAAC;AAgBD,MAAa,kBAAmB,SAAQ,KAAK;IAC3C,YACE,OAAe,EACC,OAAmC,EACnC,WAAW,KAAK,EAChB,YAAY,IAAI;QAEhC,KAAK,CAAC,OAAO,CAAC,CAAC;QAJC,YAAO,GAAP,OAAO,CAA4B;QACnC,aAAQ,GAAR,QAAQ,CAAQ;QAChB,cAAS,GAAT,SAAS,CAAO;QAGhC,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;IACnC,CAAC;CACF;AAVD,gDAUC;AAED;;;;GAIG;AACH,SAAgB,oBAAoB,CAClC,IAA+B,EAC/B,OAAe;IAEf,MAAM,OAAO,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAC9D,OAAO,OAAO,CAAC,CAAC,CAAC,IAAI,OAAO,MAAM,OAAO,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC;AACzD,CAAC;AAEM,KAAK,UAAU,SAAS,CAC7B,IASC,EACD,OAMC;IAED,IAAI,CAAC,OAAO,CAAC,aAAa,EAAE,CAAC;QAC3B,MAAM,IAAI,kBAAkB,CAC1B,4BAA4B,EAC5B,SAAS,EACT,KAAK,EACL,KAAK,CACN,CAAC;IACJ,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,IAAI,KAAK,CAAC,CAC7C,mCAAmC,EACnC;QACE,MAAM,EAAE,MAAM;QACd,OAAO,EAAE;YACP,MAAM,EAAE,kBAAkB;YAC1B,cAAc,EAAE,kBAAkB;YAClC,yBAAyB,EAAE,OAAO,CAAC,aAAa;SACjD;QACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;YACnB,IAAI,EAAE,IAAI,CAAC,IAAI,IAAI,2CAA2C;YAC9D,EAAE,EAAE,IAAI,CAAC,EAAE;YACX,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,QAAQ,EAAE,IAAI,CAAC,IAAI;YACnB,QAAQ,EAAE,IAAI,CAAC,IAAI;YACnB,aAAa,EAAE,UAAU;YACzB,GAAG,EAAE,IAAI,CAAC,GAAG;YACb,QAAQ,EAAE,IAAI,CAAC,QAAQ;SACxB,CAAC;KACH,CACF,CAAC;IAEF,MAAM,gBAAgB,GAAY,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC1E,MAAM,iBAAiB,GACrB,aAAa,CAAC,+BAA+B,EAAE,gBAAgB,CAAC;QAChE,aAAa,CACX,6BAA6B,EAC7B,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAC1C,CAAC;IAEJ,IACE,CAAC,QAAQ,CAAC,EAAE;QACZ,CAAC,iBAAiB,KAAK,SAAS,IAAI,iBAAiB,KAAK,CAAC,CAAC,EAC5D,CAAC;QACD,MAAM,iBAAiB,GACrB,QAAQ,CAAC,MAAM,KAAK,GAAG,IAAI,iBAAiB,KAAK,GAAG,CAAC;QACvD,MAAM,OAAO,GAAgC;YAC3C,QAAQ,EAAE,UAAU;YACpB,gBAAgB,EAAE,iBAAiB,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,UAAU;YACtE,UAAU,EAAE,QAAQ,CAAC,MAAM;YAC3B,GAAG,CAAC,iBAAiB,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,iBAAiB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAClE,CAAC;QAEF,IAAI,iBAAiB,EAAE,CAAC;YACtB,MAAM,IAAI,kBAAkB,CAC1B,gCAAgC,EAChC,OAAO,EACP,IAAI,EACJ,KAAK,CACN,CAAC;QACJ,CAAC;QAED,MAAM,IAAI,kBAAkB,CAC1B,6BAA6B,EAC7B,OAAO,EACP,KAAK,EACL,QAAQ,CAAC,MAAM,KAAK,GAAG,IAAI,QAAQ,CAAC,MAAM,IAAI,GAAG,CAClD,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,6BAA6B,CAAC,SAAS,CAAC,gBAAgB,CAAC,CAAC;IACzE,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,MAAM,IAAI,kBAAkB,CAAC,uCAAuC,EAAE;YACpE,QAAQ,EAAE,UAAU;YACpB,gBAAgB,EAAE,UAAU;YAC5B,UAAU,EAAE,QAAQ,CAAC,MAAM;YAC3B,GAAG,CAAC,iBAAiB,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,iBAAiB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAClE,CAAC,CAAC;IACL,CAAC;IAED,OAAO;QACL,UAAU,EAAE,QAAQ,CAAC,MAAM;QAC3B,SAAS,EAAE,MAAM,CAAC,IAAI,CAAC,SAAS;QAChC,WAAW,EAAE,MAAM,CAAC,IAAI,CAAC,WAAW;KACrC,CAAC;AACJ,CAAC;AAEM,KAAK,UAAU,qBAAqB,CAAC,UAA8B;IACxE,MAAM,QAAQ,GAAG,CAAC,MAAM,OAAO,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC,CAAC,MAAM,CAC5D,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,KAAK,UAAU,CACzC,CAAC;IACF,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IAElC,MAAM,uBAAuB,GAAG,QAAQ,CAAC,IAAI,CAC3C,CAAC,OAAO,EAAE,EAAE,CACV,OAAO,CAAC,MAAM,YAAY,kBAAkB,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAC3E,EAAE,MAAM,CAAC;IACV,MAAM,WAAW,GAAG,QAAQ,CAAC,KAAK,CAChC,CAAC,OAAO,EAAE,EAAE,CACV,OAAO,CAAC,MAAM,YAAY,kBAAkB,IAAI,OAAO,CAAC,MAAM,CAAC,QAAQ,CAC1E,CAAC;IACF,MAAM,kBAAkB,GAAG,QAAQ,CAAC,IAAI,CACtC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,YAAY,kBAAkB,CAC1D,EAAE,MAAM,CAAC;IACV,MAAM,cAAc,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAClD,IACE,CAAC,CAAC,OAAO,CAAC,MAAM,YAAY,kBAAkB,CAAC;YAC/C,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO;YACvB,CAAC,CAAC,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,EACvC,CAAC;YACD,OAAO,EAAE,CAAC;QACZ,CAAC;QACD,MAAM,EAAE,UAAU,EAAE,iBAAiB,EAAE,QAAQ,EAAE,gBAAgB,EAAE,GACjE,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC;QACzB,OAAO;YACL;gBACE,UAAU;gBACV,QAAQ;gBACR,gBAAgB;gBAChB,GAAG,CAAC,iBAAiB,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,iBAAiB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAClE;SACF,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,MAAM,OAAO,GAA8B;QACzC,YAAY,EAAE,QAAQ,CAAC,MAAM;QAC7B,QAAQ,EAAE,cAAc;KACzB,CAAC;IACF,IAAI,uBAAuB,YAAY,kBAAkB,EAAE,CAAC;QAC1D,IAAI,uBAAuB,CAAC,OAAO,KAAK,4BAA4B,EAAE,CAAC;YACrE,MAAM,IAAI,kBAAkB,CAC1B,4BAA4B,EAC5B,OAAO,EACP,KAAK,EACL,KAAK,CACN,CAAC;QACJ,CAAC;QACD,MAAM,IAAI,kBAAkB,CAC1B,6BAA6B,EAC7B,OAAO,EACP,KAAK,EACL,uBAAuB,CAAC,SAAS,CAClC,CAAC;IACJ,CAAC;IACD,IAAI,WAAW,IAAI,kBAAkB,YAAY,kBAAkB,EAAE,CAAC;QACpE,MAAM,IAAI,kBAAkB,CAC1B,gCAAgC,EAChC,OAAO,EACP,IAAI,EACJ,KAAK,CACN,CAAC;IACJ,CAAC;IACD,MAAM,IAAI,kBAAkB,CAAC,uBAAuB,EAAE,OAAO,CAAC,CAAC;AACjE,CAAC","sourcesContent":["import { day } from '@vrplatform/utils';\nimport { z } from 'zod';\n\nconst postmarkErrorCodeSchema = z.number().int().nonnegative();\nconst postmarkResponseErrorCodeSchema = z\n .object({ ErrorCode: postmarkErrorCodeSchema })\n .transform(({ ErrorCode }) => ErrorCode);\nconst postmarkHeaderErrorCodeSchema = z\n .string()\n .regex(/^\\d+$/)\n .transform(Number)\n .pipe(postmarkErrorCodeSchema);\nconst postmarkSuccessResponseSchema = z.object({\n ErrorCode: z.literal(0),\n MessageID: z.string().min(1),\n SubmittedAt: z.string().refine((value) => day(value).isValid()),\n});\n\nfunction parseOptional<T>(schema: z.ZodType<T>, value: unknown) {\n const result = schema.safeParse(value);\n if (result.success) return result.data;\n}\n\ntype EmailDeliveryFailureDetails = {\n httpStatus: number;\n postmarkErrorCode?: number;\n provider: 'postmark';\n providerCategory: 'inactiveRecipient' | 'rejected';\n};\n\ntype EmailDeliveryErrorDetails =\n | EmailDeliveryFailureDetails\n | {\n failureCount: number;\n failures: EmailDeliveryFailureDetails[];\n };\n\nexport class EmailDeliveryError extends Error {\n constructor(\n message: string,\n public readonly details?: EmailDeliveryErrorDetails,\n public readonly expected = false,\n public readonly retryable = true\n ) {\n super(message);\n this.name = 'EmailDeliveryError';\n }\n}\n\n/**\n * Formats a verified sender address with a display name for the From header.\n * Registrations store bare addresses; recipients should still see the company\n * name the way they see `VRPlatform <notifications@vrplatform.app>`.\n */\nexport function formatSenderIdentity(\n name: string | null | undefined,\n address: string\n) {\n const display = (name ?? '').replace(/[\\r\\n\"<>]/g, '').trim();\n return display ? `\"${display}\" <${address}>` : address;\n}\n\nexport async function sendEmail(\n data: {\n to: string;\n subject: string;\n text: string;\n html: string;\n replyTo?: string;\n tag?: string;\n metadata?: Record<string, string>;\n from?: string;\n },\n options: {\n fetch?: (\n input: string | URL | Request,\n init?: RequestInit\n ) => Promise<Response>;\n postmarkToken?: string;\n }\n) {\n if (!options.postmarkToken) {\n throw new EmailDeliveryError(\n 'Postmark is not configured',\n undefined,\n false,\n false\n );\n }\n\n const response = await (options.fetch ?? fetch)(\n 'https://api.postmarkapp.com/email',\n {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n 'X-Postmark-Server-Token': options.postmarkToken,\n },\n body: JSON.stringify({\n From: data.from ?? 'VRPlatform <notifications@vrplatform.app>',\n To: data.to,\n Subject: data.subject,\n ReplyTo: data.replyTo,\n TextBody: data.text,\n HtmlBody: data.html,\n MessageStream: 'outbound',\n Tag: data.tag,\n Metadata: data.metadata,\n }),\n }\n );\n\n const providerResponse: unknown = await response.json().catch(() => ({}));\n const postmarkErrorCode =\n parseOptional(postmarkResponseErrorCodeSchema, providerResponse) ??\n parseOptional(\n postmarkHeaderErrorCodeSchema,\n response.headers.get('X-PM-ApiErrorCode')\n );\n\n if (\n !response.ok ||\n (postmarkErrorCode !== undefined && postmarkErrorCode !== 0)\n ) {\n const inactiveRecipient =\n response.status === 422 && postmarkErrorCode === 406;\n const details: EmailDeliveryFailureDetails = {\n provider: 'postmark',\n providerCategory: inactiveRecipient ? 'inactiveRecipient' : 'rejected',\n httpStatus: response.status,\n ...(postmarkErrorCode !== undefined ? { postmarkErrorCode } : {}),\n };\n\n if (inactiveRecipient) {\n throw new EmailDeliveryError(\n 'Postmark recipient is inactive',\n details,\n true,\n false\n );\n }\n\n throw new EmailDeliveryError(\n 'Postmark rejected the email',\n details,\n false,\n response.status === 429 || response.status >= 500\n );\n }\n\n const result = postmarkSuccessResponseSchema.safeParse(providerResponse);\n if (!result.success) {\n throw new EmailDeliveryError('Postmark returned an invalid response', {\n provider: 'postmark',\n providerCategory: 'rejected',\n httpStatus: response.status,\n ...(postmarkErrorCode !== undefined ? { postmarkErrorCode } : {}),\n });\n }\n\n return {\n httpStatus: response.status,\n messageId: result.data.MessageID,\n submittedAt: result.data.SubmittedAt,\n };\n}\n\nexport async function settleEmailDeliveries(deliveries: Promise<unknown>[]) {\n const failures = (await Promise.allSettled(deliveries)).filter(\n (result) => result.status === 'rejected'\n );\n if (failures.length === 0) return;\n\n const unexpectedDeliveryError = failures.find(\n (failure) =>\n failure.reason instanceof EmailDeliveryError && !failure.reason.expected\n )?.reason;\n const allExpected = failures.every(\n (failure) =>\n failure.reason instanceof EmailDeliveryError && failure.reason.expected\n );\n const firstDeliveryError = failures.find(\n (failure) => failure.reason instanceof EmailDeliveryError\n )?.reason;\n const failureDetails = failures.flatMap((failure) => {\n if (\n !(failure.reason instanceof EmailDeliveryError) ||\n !failure.reason.details ||\n !('provider' in failure.reason.details)\n ) {\n return [];\n }\n const { httpStatus, postmarkErrorCode, provider, providerCategory } =\n failure.reason.details;\n return [\n {\n httpStatus,\n provider,\n providerCategory,\n ...(postmarkErrorCode !== undefined ? { postmarkErrorCode } : {}),\n },\n ];\n });\n\n const details: EmailDeliveryErrorDetails = {\n failureCount: failures.length,\n failures: failureDetails,\n };\n if (unexpectedDeliveryError instanceof EmailDeliveryError) {\n if (unexpectedDeliveryError.message === 'Postmark is not configured') {\n throw new EmailDeliveryError(\n 'Postmark is not configured',\n details,\n false,\n false\n );\n }\n throw new EmailDeliveryError(\n 'Postmark rejected the email',\n details,\n false,\n unexpectedDeliveryError.retryable\n );\n }\n if (allExpected && firstDeliveryError instanceof EmailDeliveryError) {\n throw new EmailDeliveryError(\n 'Postmark recipient is inactive',\n details,\n true,\n false\n );\n }\n throw new EmailDeliveryError('Email delivery failed', details);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"send-email.js","sourceRoot":"src/","sources":["utils/send-email.ts"],"names":[],"mappings":"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","sourcesContent":["import { day } from '@vrplatform/utils';\nimport { z } from 'zod';\n\nconst postmarkErrorCodeSchema = z.number().int().nonnegative();\nconst postmarkResponseErrorCodeSchema = z\n .object({ ErrorCode: postmarkErrorCodeSchema })\n .transform(({ ErrorCode }) => ErrorCode);\nconst postmarkHeaderErrorCodeSchema = z\n .string()\n .regex(/^\\d+$/)\n .transform(Number)\n .pipe(postmarkErrorCodeSchema);\nconst postmarkSuccessResponseSchema = z.object({\n ErrorCode: z.literal(0),\n MessageID: z.string().min(1),\n SubmittedAt: z.string().refine((value) => day(value).isValid()),\n});\n\nfunction parseOptional<T>(schema: z.ZodType<T>, value: unknown) {\n const result = schema.safeParse(value);\n if (result.success) return result.data;\n}\n\ntype EmailDeliveryFailureDetails = {\n httpStatus: number;\n postmarkErrorCode?: number;\n provider: 'postmark';\n providerCategory: 'inactiveRecipient' | 'rejected';\n};\n\ntype EmailDeliveryErrorDetails =\n | EmailDeliveryFailureDetails\n | {\n failureCount: number;\n failures: EmailDeliveryFailureDetails[];\n };\n\nexport class EmailDeliveryError extends Error {\n constructor(\n message: string,\n public readonly details?: EmailDeliveryErrorDetails,\n public readonly expected = false,\n public readonly retryable = true\n ) {\n super(message);\n this.name = 'EmailDeliveryError';\n }\n}\n\n/**\n * Formats a verified sender address with a display name for the From header.\n * Registrations store bare addresses; recipients should still see the company\n * name the way they see `VRPlatform <notifications@vrplatform.app>`.\n */\nexport function formatSenderIdentity(\n name: string | null | undefined,\n address: string\n) {\n const display = (name ?? '').replace(/[\\r\\n\"<>]/g, '').trim();\n return display ? `\"${display}\" <${address}>` : address;\n}\n\nexport async function sendEmail(\n data: {\n to: string;\n subject: string;\n text: string;\n html: string;\n replyTo?: string;\n tag?: string;\n metadata?: Record<string, string>;\n from?: string;\n headers?: Record<string, string>;\n },\n options: {\n fetch?: (\n input: string | URL | Request,\n init?: RequestInit\n ) => Promise<Response>;\n postmarkToken?: string;\n }\n) {\n if (!options.postmarkToken) {\n throw new EmailDeliveryError(\n 'Postmark is not configured',\n undefined,\n false,\n false\n );\n }\n\n const response = await (options.fetch ?? fetch)(\n 'https://api.postmarkapp.com/email',\n {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n 'X-Postmark-Server-Token': options.postmarkToken,\n },\n body: JSON.stringify({\n From: data.from ?? 'VRPlatform <notifications@vrplatform.app>',\n To: data.to,\n Subject: data.subject,\n ReplyTo: data.replyTo,\n TextBody: data.text,\n HtmlBody: data.html,\n MessageStream: 'outbound',\n Tag: data.tag,\n Metadata: data.metadata,\n Headers: data.headers\n ? Object.entries(data.headers).map(([Name, Value]) => ({\n Name,\n Value,\n }))\n : undefined,\n }),\n }\n );\n\n const providerResponse: unknown = await response.json().catch(() => ({}));\n const postmarkErrorCode =\n parseOptional(postmarkResponseErrorCodeSchema, providerResponse) ??\n parseOptional(\n postmarkHeaderErrorCodeSchema,\n response.headers.get('X-PM-ApiErrorCode')\n );\n\n if (\n !response.ok ||\n (postmarkErrorCode !== undefined && postmarkErrorCode !== 0)\n ) {\n const inactiveRecipient =\n response.status === 422 && postmarkErrorCode === 406;\n const details: EmailDeliveryFailureDetails = {\n provider: 'postmark',\n providerCategory: inactiveRecipient ? 'inactiveRecipient' : 'rejected',\n httpStatus: response.status,\n ...(postmarkErrorCode !== undefined ? { postmarkErrorCode } : {}),\n };\n\n if (inactiveRecipient) {\n throw new EmailDeliveryError(\n 'Postmark recipient is inactive',\n details,\n true,\n false\n );\n }\n\n throw new EmailDeliveryError(\n 'Postmark rejected the email',\n details,\n false,\n response.status === 429 || response.status >= 500\n );\n }\n\n const result = postmarkSuccessResponseSchema.safeParse(providerResponse);\n if (!result.success) {\n throw new EmailDeliveryError('Postmark returned an invalid response', {\n provider: 'postmark',\n providerCategory: 'rejected',\n httpStatus: response.status,\n ...(postmarkErrorCode !== undefined ? { postmarkErrorCode } : {}),\n });\n }\n\n return {\n httpStatus: response.status,\n messageId: result.data.MessageID,\n submittedAt: result.data.SubmittedAt,\n };\n}\n\nexport async function settleEmailDeliveries(deliveries: Promise<unknown>[]) {\n const failures = (await Promise.allSettled(deliveries)).filter(\n (result) => result.status === 'rejected'\n );\n if (failures.length === 0) return;\n\n const unexpectedDeliveryError = failures.find(\n (failure) =>\n failure.reason instanceof EmailDeliveryError && !failure.reason.expected\n )?.reason;\n const allExpected = failures.every(\n (failure) =>\n failure.reason instanceof EmailDeliveryError && failure.reason.expected\n );\n const firstDeliveryError = failures.find(\n (failure) => failure.reason instanceof EmailDeliveryError\n )?.reason;\n const failureDetails = failures.flatMap((failure) => {\n if (\n !(failure.reason instanceof EmailDeliveryError) ||\n !failure.reason.details ||\n !('provider' in failure.reason.details)\n ) {\n return [];\n }\n const { httpStatus, postmarkErrorCode, provider, providerCategory } =\n failure.reason.details;\n return [\n {\n httpStatus,\n provider,\n providerCategory,\n ...(postmarkErrorCode !== undefined ? { postmarkErrorCode } : {}),\n },\n ];\n });\n\n const details: EmailDeliveryErrorDetails = {\n failureCount: failures.length,\n failures: failureDetails,\n };\n if (unexpectedDeliveryError instanceof EmailDeliveryError) {\n if (unexpectedDeliveryError.message === 'Postmark is not configured') {\n throw new EmailDeliveryError(\n 'Postmark is not configured',\n details,\n false,\n false\n );\n }\n throw new EmailDeliveryError(\n 'Postmark rejected the email',\n details,\n false,\n unexpectedDeliveryError.retryable\n );\n }\n if (allExpected && firstDeliveryError instanceof EmailDeliveryError) {\n throw new EmailDeliveryError(\n 'Postmark recipient is inactive',\n details,\n true,\n false\n );\n }\n throw new EmailDeliveryError('Email delivery failed', details);\n}\n"]}
|
|
@@ -12,4 +12,4 @@ export type ActionEmailData = {
|
|
|
12
12
|
export interface ActionEmailTemplateProps extends EmailBaseProps {
|
|
13
13
|
data: ActionEmailData;
|
|
14
14
|
}
|
|
15
|
-
export declare const ActionEmailTemplate: ({ data, tenant, themeColors, mode, user, listing, month, }: ActionEmailTemplateProps) => import("react").JSX.Element;
|
|
15
|
+
export declare const ActionEmailTemplate: ({ data, tenant, themeColors, mode, user, listing, month, unsubscribeHref, }: ActionEmailTemplateProps) => import("react").JSX.Element;
|
|
@@ -4,8 +4,8 @@ import { Text } from '@react-email/text';
|
|
|
4
4
|
import { EmailContainer } from './EmailContainer';
|
|
5
5
|
import { parseEmailHtml } from './utils';
|
|
6
6
|
import { Button } from './Button';
|
|
7
|
-
export const ActionEmailTemplate = ({ data, tenant, themeColors, mode, user, listing, month, }) => {
|
|
8
|
-
return (_jsxs(EmailContainer, { title: data.subject, shortDescription: data.shortDescription, tenant: tenant, mode: mode, children: [_jsx(Heading, { style: {
|
|
7
|
+
export const ActionEmailTemplate = ({ data, tenant, themeColors, mode, user, listing, month, unsubscribeHref, }) => {
|
|
8
|
+
return (_jsxs(EmailContainer, { title: data.subject, shortDescription: data.shortDescription, tenant: tenant, mode: mode, unsubscribeHref: unsubscribeHref, children: [_jsx(Heading, { style: {
|
|
9
9
|
fontSize: 28,
|
|
10
10
|
fontWeight: 600,
|
|
11
11
|
marginBottom: '50px',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ActionEmailTemplate.js","sourceRoot":"src/","sources":["components/ActionEmailTemplate.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAC/C,OAAO,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AACzC,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAElD,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAiBlC,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,EAClC,IAAI,EACJ,MAAM,EACN,WAAW,EACX,IAAI,EACJ,IAAI,EACJ,OAAO,EACP,KAAK,
|
|
1
|
+
{"version":3,"file":"ActionEmailTemplate.js","sourceRoot":"src/","sources":["components/ActionEmailTemplate.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAC/C,OAAO,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AACzC,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAElD,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAiBlC,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,EAClC,IAAI,EACJ,MAAM,EACN,WAAW,EACX,IAAI,EACJ,IAAI,EACJ,OAAO,EACP,KAAK,EACL,eAAe,GACU,EAAE,EAAE;IAC7B,OAAO,CACL,MAAC,cAAc,IACb,KAAK,EAAE,IAAI,CAAC,OAAO,EACnB,gBAAgB,EAAE,IAAI,CAAC,gBAAgB,EACvC,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,IAAI,EACV,eAAe,EAAE,eAAe,aAEhC,KAAC,OAAO,IACN,KAAK,EAAE;oBACL,QAAQ,EAAE,EAAE;oBACZ,UAAU,EAAE,GAAG;oBACf,YAAY,EAAE,MAAM;iBACrB,YAEA,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,cAAc,MAAM,CAAC,IAAI,GAAG,GAC5C,EAET,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,CACtB,KAAC,IAAI,IACH,KAAK,EAAE;oBACL,OAAO,EAAE,MAAM;oBACf,eAAe,EAAE,SAAS;oBAC1B,YAAY,EAAE,KAAK;oBACnB,YAAY,EAAE,MAAM;iBACrB,YAEA,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,GACd,CACR,EAED;gBACE,sEAAsE;gBACtE,uBAAuB,EAAE;oBACvB,MAAM,EAAE,cAAc,CAAC,IAAI,CAAC,OAAO,EAAE;wBACnC,IAAI,EAAE,IAAI,CAAC,IAAI;wBACf,QAAQ,EAAE,IAAI,CAAC,SAAS,IAAI,EAAE;wBAC9B,IAAI,EAAE,KAAK,IAAI,EAAE;wBACjB,WAAW,EAAE,OAAO,IAAI,EAAE;qBAC3B,CAAC;iBACH,GACD,EAEF,KAAC,MAAM,IACL,IAAI,EAAE,IAAI,CAAC,IAAI,EACf,IAAI,EAAE,IAAI,iBACE,sBAAsB,EAClC,KAAK,EAAE,IAAI,CAAC,UAAU,EAAE,IAAI,EAAE,IAAI,mBAAmB,EACrD,WAAW,EAAE,WAAW,GACxB,EAEF;gBACE,sEAAsE;gBACtE,uBAAuB,EAAE;oBACvB,MAAM,EAAE,cAAc,CAAC,IAAI,CAAC,OAAO,EAAE;wBACnC,IAAI,EAAE,IAAI,CAAC,IAAI;wBACf,QAAQ,EAAE,IAAI,CAAC,SAAS,IAAI,EAAE;wBAC9B,IAAI,EAAE,KAAK,IAAI,EAAE;wBACjB,WAAW,EAAE,OAAO,IAAI,EAAE;qBAC3B,CAAC;iBACH,GACD,IACa,CAClB,CAAC;AACJ,CAAC,CAAC","sourcesContent":["import { Heading } from '@react-email/heading';\nimport { Text } from '@react-email/text';\nimport { EmailContainer } from './EmailContainer';\nimport type { EmailBaseProps } from '../types';\nimport { parseEmailHtml } from './utils';\nimport { Button } from './Button';\n\nexport type ActionEmailData = {\n subject: string;\n shortDescription: string;\n banner?: string;\n header: string;\n bodyOne: string;\n bodyTwo: string;\n href: string;\n buttonText: string;\n};\n\nexport interface ActionEmailTemplateProps extends EmailBaseProps {\n data: ActionEmailData;\n}\n\nexport const ActionEmailTemplate = ({\n data,\n tenant,\n themeColors,\n mode,\n user,\n listing,\n month,\n unsubscribeHref,\n}: ActionEmailTemplateProps) => {\n return (\n <EmailContainer\n title={data.subject}\n shortDescription={data.shortDescription}\n tenant={tenant}\n mode={mode}\n unsubscribeHref={unsubscribeHref}\n >\n <Heading\n style={{\n fontSize: 28,\n fontWeight: 600,\n marginBottom: '50px',\n }}\n >\n {data.header?.trim() || `Welcome to ${tenant.name}!`}\n </Heading>\n\n {data.banner?.trim() && (\n <Text\n style={{\n padding: '24px',\n backgroundColor: '#f2f3f3',\n borderRadius: '4px',\n marginBottom: '40px',\n }}\n >\n {data.banner.trim()}\n </Text>\n )}\n\n <div\n // biome-ignore lint/security/noDangerouslySetInnerHtml: <explanation>\n dangerouslySetInnerHTML={{\n __html: parseEmailHtml(data.bodyOne, {\n href: data.href,\n userName: user.firstName || '',\n date: month || '',\n listingName: listing || '',\n }),\n }}\n />\n\n <Button\n href={data.href}\n mode={mode}\n data-testid=\"accept-invite-button\"\n label={data.buttonText?.trim() || 'Accept Invitation'}\n themeColors={themeColors}\n />\n\n <div\n // biome-ignore lint/security/noDangerouslySetInnerHtml: <explanation>\n dangerouslySetInnerHTML={{\n __html: parseEmailHtml(data.bodyTwo, {\n href: data.href,\n userName: user.firstName || '',\n date: month || '',\n listingName: listing || '',\n }),\n }}\n />\n </EmailContainer>\n );\n};\n"]}
|
|
@@ -27,7 +27,7 @@ export const EmailContainer = (props) => {
|
|
|
27
27
|
format: 'woff2',
|
|
28
28
|
}, fontWeight: 600, fontStyle: "normal" })] }), shortDescription && _jsx(Preview, { children: shortDescription }), _jsx(Body, { style: contentWrapperStyle, children: _jsx(Content, { ...props, children: children }) })] }));
|
|
29
29
|
};
|
|
30
|
-
const Content = ({ children, tenant: { logo: tenantLogo, name, showPlatformBranding = true }, mode, }) => {
|
|
30
|
+
const Content = ({ children, tenant: { logo: tenantLogo, name, showPlatformBranding = true }, mode, unsubscribeHref, }) => {
|
|
31
31
|
return (_jsxs(_Fragment, { children: [tenantLogo ? (_jsx("div", { style: {
|
|
32
32
|
marginBottom: '60px',
|
|
33
33
|
padding: '5px',
|
|
@@ -39,7 +39,7 @@ const Content = ({ children, tenant: { logo: tenantLogo, name, showPlatformBrand
|
|
|
39
39
|
alignItems: 'center',
|
|
40
40
|
}, children: _jsx(Img, { src: tenantLogo, alt: 'Logo', width: 70 }) })) : !showPlatformBranding ? (_jsx(Text, { children: name })) : mode === 'send' ? (_jsx(Img, { src: 'https://staging.portal.vrplatform.app/logo.png', alt: 'Logo', width: 70, style: { marginBottom: '60px' } })) : (_jsx("div", { style: {
|
|
41
41
|
width: '60px',
|
|
42
|
-
}, children: _jsx(VrpLogo, {}) })), children, showPlatformBranding ? _jsx(Footer, {}) : _jsx(Text, { children: name })] }));
|
|
42
|
+
}, children: _jsx(VrpLogo, {}) })), children, showPlatformBranding ? _jsx(Footer, {}) : _jsx(Text, { children: name }), unsubscribeHref && (_jsx(Text, { style: { color: '#737373' }, children: _jsx("a", { href: unsubscribeHref, style: { color: '#737373' }, children: "Unsubscribe from owner statement emails" }) }))] }));
|
|
43
43
|
};
|
|
44
44
|
const Footer = () => (_jsxs(_Fragment, { children: [_jsx(Hr, { style: {
|
|
45
45
|
marginTop: '30px',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EmailContainer.js","sourceRoot":"src/","sources":["components/EmailContainer.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AACzC,OAAO,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AACzC,OAAO,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AACzC,OAAO,EAAE,EAAE,EAAE,MAAM,iBAAiB,CAAC;AACrC,OAAO,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AACzC,OAAO,EAAE,GAAG,EAAE,MAAM,kBAAkB,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAC/C,OAAO,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AACzC,OAAO,EAAE,GAAG,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"EmailContainer.js","sourceRoot":"src/","sources":["components/EmailContainer.tsx"],"names":[],"mappings":"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","sourcesContent":["import { Body } from '@react-email/body';\nimport { Font } from '@react-email/font';\nimport { Head } from '@react-email/head';\nimport { Hr } from '@react-email/hr';\nimport { Html } from '@react-email/html';\nimport { Img } from '@react-email/img';\nimport { Preview } from '@react-email/preview';\nimport { Text } from '@react-email/text';\nimport { day } from '@vrplatform/utils';\nimport type { EmailBaseProps } from '../types';\n\ntype Props = {\n children: React.ReactNode;\n title: string;\n tenant: Omit<EmailBaseProps['tenant'], 'name'> & { name?: string };\n mode: EmailBaseProps['mode'];\n shortDescription?: string;\n unsubscribeHref?: string;\n};\n\nexport const EmailContainer = (props: Props) => {\n const { children, shortDescription, title } = props;\n\n const contentWrapperStyle = {\n paddingInline: '20px',\n paddingBlock: '10px',\n maxWidth: '500px',\n margin: '0 auto',\n };\n\n if (props.mode !== 'send') {\n return (\n <div style={contentWrapperStyle}>\n <Content {...props}>{children}</Content>\n </div>\n );\n }\n\n return (\n <Html lang=\"en\">\n <Head>\n <title>{title}</title>\n <Font\n fontFamily=\"Inter\"\n fallbackFontFamily=\"Verdana\"\n webFont={{\n url: 'https://fonts.gstatic.com/s/inter/v13/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7W0Q5nw.woff2',\n format: 'woff2',\n }}\n fontWeight={400}\n fontStyle=\"normal\"\n />\n <Font\n fontFamily=\"Inter\"\n fallbackFontFamily=\"Verdana\"\n webFont={{\n url: 'https://fonts.gstatic.com/s/inter/v13/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7W0Q5nw.woff2',\n format: 'woff2',\n }}\n fontWeight={600}\n fontStyle=\"normal\"\n />\n </Head>\n {shortDescription && <Preview>{shortDescription}</Preview>}\n <Body style={contentWrapperStyle}>\n <Content {...props}>{children}</Content>\n </Body>\n </Html>\n );\n};\n\nconst Content = ({\n children,\n tenant: { logo: tenantLogo, name, showPlatformBranding = true },\n mode,\n unsubscribeHref,\n}: Props) => {\n return (\n <>\n {tenantLogo ? (\n <div\n style={{\n marginBottom: '60px',\n padding: '5px',\n // border: '1px solid rgb(233,236,239)',\n borderRadius: '5px',\n width: '80px',\n display: 'flex',\n justifyContent: 'center',\n alignItems: 'center',\n }}\n >\n <Img src={tenantLogo} alt={'Logo'} width={70} />\n </div>\n ) : !showPlatformBranding ? (\n <Text>{name}</Text>\n ) : mode === 'send' ? (\n <Img\n src={'https://staging.portal.vrplatform.app/logo.png'}\n alt={'Logo'}\n width={70}\n style={{ marginBottom: '60px' }}\n />\n ) : (\n <div\n style={{\n width: '60px',\n }}\n >\n <VrpLogo />\n </div>\n )}\n\n {children}\n\n {showPlatformBranding ? <Footer /> : <Text>{name}</Text>}\n {unsubscribeHref && (\n <Text style={{ color: '#737373' }}>\n <a href={unsubscribeHref} style={{ color: '#737373' }}>\n Unsubscribe from owner statement emails\n </a>\n </Text>\n )}\n </>\n );\n};\n\nconst Footer = () => (\n <>\n <Hr\n style={{\n marginTop: '30px',\n marginBottom: '50px',\n }}\n />\n\n <Text\n style={{\n color: '#737373',\n }}\n >\n © {day().year()} VR Technologies. All rights reserved.\n </Text>\n <Text\n style={{\n color: '#737373',\n }}\n >\n VR Technologies, LLC, a company incorporated and registered in the United\n States of America with company number 20201965158\n </Text>\n </>\n);\n\nconst VrpLogo = () => (\n <svg\n width=\"100%\"\n height=\"100%\"\n viewBox=\"0 0 28 29\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path\n d=\"M18.2549 8.25161C18.2549 5.89704 20.1636 3.98828 22.5182 3.98828C24.8728 3.98828 26.7815 5.89704 26.7815 8.25161C26.7815 10.6062 24.8728 12.5149 22.5182 12.5149C20.1636 12.5149 18.2549 10.6062 18.2549 8.25161Z\"\n fill=\"#172554\"\n />\n <path\n d=\"M7.02441 8.46593C7.02441 6.01867 9.20858 3.99805 11.6558 3.99805C14.1031 3.99805 15.0558 5.59785 16.5612 7.52728C17.1957 8.34054 20.7921 13.3413 20.7921 13.3413C20.7921 13.3413 21.566 14.6863 21.566 15.7894C21.566 18.5287 17.1993 22.8381 17.1993 22.8381L7.66841 10.6799C7.66841 10.6799 7.02441 10.0502 7.02441 8.46593Z\"\n fill=\"#172554\"\n fillOpacity=\"0.8\"\n />\n <path\n d=\"M1.24707 8.42687C1.24707 5.97961 3.43123 3.95898 5.8785 3.95898C8.32576 3.95898 9.27845 5.55879 10.7839 7.48824C11.4184 8.30147 14.6041 12.4888 16.8247 15.5727C18.3585 17.703 18.474 17.705 18.474 20.3439C18.474 22.9827 15.1592 24.1379 14.1483 24.1914C12.0203 24.304 10.5418 22.4681 10.5418 22.4681L1.89106 10.6408C1.89106 10.6408 1.24707 10.0111 1.24707 8.42687Z\"\n fill=\"#172554\"\n />\n </svg>\n);\n"]}
|
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ActionEmailData, ActionEmailTemplateProps } from '../components/ActionEmailTemplate';
|
|
2
2
|
export declare const getDefaultOwnerStatementEmailData: (mode: "tag" | "text", args: {
|
|
3
3
|
appName: string;
|
|
4
4
|
ownerStatementHref: string;
|
|
5
5
|
listingName: string;
|
|
6
6
|
userName: string;
|
|
7
7
|
month: string;
|
|
8
|
+
/** The owner opens the statement through an emailed link, without a login. */
|
|
9
|
+
noLogin?: boolean;
|
|
8
10
|
}) => ActionEmailData;
|
|
9
|
-
export declare const OwnerStatementEmailTemplate: (
|
|
11
|
+
export declare const OwnerStatementEmailTemplate: ({ data, tenant, themeColors, mode, user, listing, month, unsubscribeHref, renewHref, }: ActionEmailTemplateProps & {
|
|
12
|
+
/** Requests a new no-login link once the emailed one has expired. */
|
|
13
|
+
renewHref?: string;
|
|
14
|
+
}) => import("react").JSX.Element;
|
|
@@ -1,11 +1,32 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Heading } from '@react-email/heading';
|
|
3
|
+
import { Text } from '@react-email/text';
|
|
2
4
|
import { EMAIL_ACTION_LINK_COMPONENT, EMAIL_DATE_COMPONENT, EMAIL_LISTING_COMPONENT, EMAIL_USER_NAME_COMPONENT, } from '../extensions';
|
|
3
|
-
import {
|
|
5
|
+
import { Button } from '../components/Button';
|
|
6
|
+
import { EmailContainer } from '../components/EmailContainer';
|
|
7
|
+
import { parseEmailHtml } from '../components/utils';
|
|
4
8
|
export const getDefaultOwnerStatementEmailData = (mode, args) => {
|
|
5
9
|
const ownerStatementHref = mode === 'tag' ? EMAIL_ACTION_LINK_COMPONENT : args.ownerStatementHref;
|
|
6
10
|
const listingName = mode === 'tag' ? EMAIL_LISTING_COMPONENT : args.listingName;
|
|
7
11
|
const month = mode === 'tag' ? EMAIL_DATE_COMPONENT : args.month;
|
|
8
12
|
const userName = mode === 'tag' ? EMAIL_USER_NAME_COMPONENT : args.userName;
|
|
13
|
+
if (args.noLogin) {
|
|
14
|
+
// Subject and preview text are plain text without tokens, so a saved
|
|
15
|
+
// template must not keep the editor's sample month and listing.
|
|
16
|
+
return {
|
|
17
|
+
subject: mode === 'tag'
|
|
18
|
+
? 'Your owner statement is ready'
|
|
19
|
+
: `Your ${args.month} owner statement for ${args.listingName}`,
|
|
20
|
+
shortDescription: mode === 'tag'
|
|
21
|
+
? 'Your owner statement is ready. Open it with one click, no login needed.'
|
|
22
|
+
: `Your ${args.month} statement for ${args.listingName} is ready. Open it with one click, no login needed.`,
|
|
23
|
+
header: 'Your owner statement is ready',
|
|
24
|
+
bodyOne: `<p>Hi ${userName},</p><p>${args.appName} published your ${month} owner statement for ${listingName}. Open it with the button below. You don't need a login.</p>`,
|
|
25
|
+
buttonText: 'View statement',
|
|
26
|
+
bodyTwo: `<p style="color:#737373;font-size:13px">If the button doesn't work, copy this link into your browser: ${ownerStatementHref}</p>`,
|
|
27
|
+
href: args.ownerStatementHref,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
9
30
|
return {
|
|
10
31
|
subject: 'Owner statement was published',
|
|
11
32
|
shortDescription: `An owner statement is ready for you. Log in to ${args.appName} now to view it.`,
|
|
@@ -16,7 +37,68 @@ export const getDefaultOwnerStatementEmailData = (mode, args) => {
|
|
|
16
37
|
href: args.ownerStatementHref,
|
|
17
38
|
};
|
|
18
39
|
};
|
|
19
|
-
|
|
20
|
-
|
|
40
|
+
const mutedColor = '#737373';
|
|
41
|
+
const borderColor = '#e5e7eb';
|
|
42
|
+
export const OwnerStatementEmailTemplate = ({ data, tenant, themeColors, mode, user, listing, month, unsubscribeHref, renewHref, }) => {
|
|
43
|
+
const parse = (html) => parseEmailHtml(html, {
|
|
44
|
+
href: data.href,
|
|
45
|
+
userName: user.firstName || '',
|
|
46
|
+
date: month || '',
|
|
47
|
+
listingName: listing || '',
|
|
48
|
+
});
|
|
49
|
+
return (_jsxs(EmailContainer, { title: data.subject, shortDescription: data.shortDescription, tenant: tenant, mode: mode, unsubscribeHref: unsubscribeHref, children: [_jsx(Text, { style: {
|
|
50
|
+
color: themeColors[8],
|
|
51
|
+
fontSize: 12,
|
|
52
|
+
fontWeight: 600,
|
|
53
|
+
letterSpacing: '0.08em',
|
|
54
|
+
textTransform: 'uppercase',
|
|
55
|
+
margin: '0 0 8px',
|
|
56
|
+
}, children: "Owner statement" }), _jsx(Heading, { style: {
|
|
57
|
+
fontSize: 26,
|
|
58
|
+
fontWeight: 600,
|
|
59
|
+
lineHeight: '34px',
|
|
60
|
+
margin: '0 0 32px',
|
|
61
|
+
}, children: data.header?.trim() || 'Your owner statement is ready' }), data.banner?.trim() && (_jsx(Text, { style: {
|
|
62
|
+
padding: '16px 20px',
|
|
63
|
+
backgroundColor: '#f8f9fa',
|
|
64
|
+
borderLeft: `3px solid ${themeColors[8]}`,
|
|
65
|
+
borderRadius: '4px',
|
|
66
|
+
margin: '0 0 32px',
|
|
67
|
+
whiteSpace: 'pre-line',
|
|
68
|
+
}, children: data.banner.trim() })), _jsx("div", {
|
|
69
|
+
// biome-ignore lint/security/noDangerouslySetInnerHtml: values are escaped by parseEmailHtml
|
|
70
|
+
dangerouslySetInnerHTML: { __html: parse(data.bodyOne) } }), (listing || month) && (_jsx("table", { role: "presentation", cellPadding: 0, cellSpacing: 0, width: "100%", style: {
|
|
71
|
+
border: `1px solid ${borderColor}`,
|
|
72
|
+
borderRadius: '8px',
|
|
73
|
+
borderCollapse: 'separate',
|
|
74
|
+
margin: '24px 0 0',
|
|
75
|
+
}, children: _jsx("tbody", { children: [
|
|
76
|
+
['Property', listing],
|
|
77
|
+
['Statement', month],
|
|
78
|
+
].map(([label, value], index) => value ? (_jsxs("tr", { children: [_jsx("td", { style: {
|
|
79
|
+
padding: '14px 20px',
|
|
80
|
+
color: mutedColor,
|
|
81
|
+
fontSize: 14,
|
|
82
|
+
borderTop: index ? `1px solid ${borderColor}` : 'none',
|
|
83
|
+
}, children: label }), _jsx("td", { style: {
|
|
84
|
+
padding: '14px 20px',
|
|
85
|
+
fontSize: 14,
|
|
86
|
+
fontWeight: 600,
|
|
87
|
+
textAlign: 'right',
|
|
88
|
+
borderTop: index ? `1px solid ${borderColor}` : 'none',
|
|
89
|
+
}, children: value })] }, label)) : null) }) })), _jsx(Button, { href: data.href, mode: mode, "data-testid": "view-owner-statement-button", label: data.buttonText?.trim() || 'View statement', themeColors: themeColors, style: { marginTop: '24px', marginBottom: '32px' } }), renewHref && (_jsxs("div", { style: {
|
|
90
|
+
padding: '20px',
|
|
91
|
+
backgroundColor: '#f8f9fa',
|
|
92
|
+
borderRadius: '8px',
|
|
93
|
+
marginBottom: '32px',
|
|
94
|
+
}, children: [_jsx(Text, { style: { fontWeight: 600, margin: '0 0 4px' }, children: "Link expired?" }), _jsx(Text, { style: { color: mutedColor, fontSize: 14, margin: 0 }, children: "For your security, this link opens only this statement and works for 7 days. After that, request a new one and we'll email it to this address." }), _jsx(Button, { href: renewHref, mode: mode, "data-testid": "renew-owner-statement-link-button", label: "Send me a new link", themeColors: themeColors, style: {
|
|
95
|
+
background: '#ffffff',
|
|
96
|
+
border: `1px solid ${themeColors[8]}`,
|
|
97
|
+
color: themeColors[8],
|
|
98
|
+
marginTop: '16px',
|
|
99
|
+
marginBottom: 0,
|
|
100
|
+
} })] })), _jsx("div", {
|
|
101
|
+
// biome-ignore lint/security/noDangerouslySetInnerHtml: values are escaped by parseEmailHtml
|
|
102
|
+
dangerouslySetInnerHTML: { __html: parse(data.bodyTwo) } })] }));
|
|
21
103
|
};
|
|
22
104
|
//# sourceMappingURL=OwnerStatementEmailTemplate.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OwnerStatementEmailTemplate.js","sourceRoot":"src/","sources":["templates/OwnerStatementEmailTemplate.tsx"],"names":[],"mappings":";AAAA,OAAO,EACL,2BAA2B,EAC3B,oBAAoB,EACpB,uBAAuB,EACvB,yBAAyB,GAC1B,MAAM,eAAe,CAAC;AACvB,OAAO,EAEL,mBAAmB,GAEpB,MAAM,mCAAmC,CAAC;AAE3C,MAAM,CAAC,MAAM,iCAAiC,GAAG,CAC/C,IAAoB,EACpB,IAMC,EACgB,EAAE;IACnB,MAAM,kBAAkB,GACtB,IAAI,KAAK,KAAK,CAAC,CAAC,CAAC,2BAA2B,CAAC,CAAC,CAAC,IAAI,CAAC,kBAAkB,CAAC;IAEzE,MAAM,WAAW,GACf,IAAI,KAAK,KAAK,CAAC,CAAC,CAAC,uBAAuB,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC;IAC9D,MAAM,KAAK,GAAG,IAAI,KAAK,KAAK,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC;IACjE,MAAM,QAAQ,GAAG,IAAI,KAAK,KAAK,CAAC,CAAC,CAAC,yBAAyB,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC;IAE5E,OAAO;QACL,OAAO,EAAE,+BAA+B;QACxC,gBAAgB,EAAE,kDAAkD,IAAI,CAAC,OAAO,kBAAkB;QAClG,MAAM,EAAE,+BAA+B;QACvC,OAAO,EAAE,SAAS,QAAQ,YAAY,KAAK,iCAAiC,WAAW,gCAAgC,IAAI,CAAC,OAAO,sEAAsE;QACzM,UAAU,EAAE,sBAAsB;QAClC,OAAO,EAAE,wGAAwG,kBAAkB,MAAM;QACzI,IAAI,EAAE,IAAI,CAAC,kBAAkB;KAC9B,CAAC;AACJ,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,2BAA2B,GAAG,CACzC,KAA+B,EAC/B,EAAE;IACF,OAAO,KAAC,mBAAmB,OAAK,KAAK,GAAI,CAAC;AAC5C,CAAC,CAAC","sourcesContent":["import {\n EMAIL_ACTION_LINK_COMPONENT,\n EMAIL_DATE_COMPONENT,\n EMAIL_LISTING_COMPONENT,\n EMAIL_USER_NAME_COMPONENT,\n} from '../extensions';\nimport {\n type ActionEmailData,\n ActionEmailTemplate,\n type ActionEmailTemplateProps,\n} from '../components/ActionEmailTemplate';\n\nexport const getDefaultOwnerStatementEmailData = (\n mode: 'tag' | 'text',\n args: {\n appName: string;\n ownerStatementHref: string;\n listingName: string;\n userName: string;\n month: string;\n }\n): ActionEmailData => {\n const ownerStatementHref =\n mode === 'tag' ? EMAIL_ACTION_LINK_COMPONENT : args.ownerStatementHref;\n\n const listingName =\n mode === 'tag' ? EMAIL_LISTING_COMPONENT : args.listingName;\n const month = mode === 'tag' ? EMAIL_DATE_COMPONENT : args.month;\n const userName = mode === 'tag' ? EMAIL_USER_NAME_COMPONENT : args.userName;\n\n return {\n subject: 'Owner statement was published',\n shortDescription: `An owner statement is ready for you. Log in to ${args.appName} now to view it.`,\n header: 'Owner statement was published',\n bodyOne: `<p>Hi ${userName},<p>Your ${month} owner statement for property ${listingName} is ready for your review on ${args.appName}.</p><p>To access your statement, please click the button below:</p>`,\n buttonText: 'View Owner statement',\n bodyTwo: `<p>If you have any trouble with the button, you can copy and paste the link below into your browser: ${ownerStatementHref}</p>`,\n href: args.ownerStatementHref,\n };\n};\n\nexport const OwnerStatementEmailTemplate = (\n props: ActionEmailTemplateProps\n) => {\n return <ActionEmailTemplate {...props} />;\n};\n"]}
|
|
1
|
+
{"version":3,"file":"OwnerStatementEmailTemplate.js","sourceRoot":"src/","sources":["templates/OwnerStatementEmailTemplate.tsx"],"names":[],"mappings":"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","sourcesContent":["import { Heading } from '@react-email/heading';\nimport { Text } from '@react-email/text';\nimport {\n EMAIL_ACTION_LINK_COMPONENT,\n EMAIL_DATE_COMPONENT,\n EMAIL_LISTING_COMPONENT,\n EMAIL_USER_NAME_COMPONENT,\n} from '../extensions';\nimport type {\n ActionEmailData,\n ActionEmailTemplateProps,\n} from '../components/ActionEmailTemplate';\nimport { Button } from '../components/Button';\nimport { EmailContainer } from '../components/EmailContainer';\nimport { parseEmailHtml } from '../components/utils';\n\nexport const getDefaultOwnerStatementEmailData = (\n mode: 'tag' | 'text',\n args: {\n appName: string;\n ownerStatementHref: string;\n listingName: string;\n userName: string;\n month: string;\n /** The owner opens the statement through an emailed link, without a login. */\n noLogin?: boolean;\n }\n): ActionEmailData => {\n const ownerStatementHref =\n mode === 'tag' ? EMAIL_ACTION_LINK_COMPONENT : args.ownerStatementHref;\n\n const listingName =\n mode === 'tag' ? EMAIL_LISTING_COMPONENT : args.listingName;\n const month = mode === 'tag' ? EMAIL_DATE_COMPONENT : args.month;\n const userName = mode === 'tag' ? EMAIL_USER_NAME_COMPONENT : args.userName;\n\n if (args.noLogin) {\n // Subject and preview text are plain text without tokens, so a saved\n // template must not keep the editor's sample month and listing.\n return {\n subject:\n mode === 'tag'\n ? 'Your owner statement is ready'\n : `Your ${args.month} owner statement for ${args.listingName}`,\n shortDescription:\n mode === 'tag'\n ? 'Your owner statement is ready. Open it with one click, no login needed.'\n : `Your ${args.month} statement for ${args.listingName} is ready. Open it with one click, no login needed.`,\n header: 'Your owner statement is ready',\n bodyOne: `<p>Hi ${userName},</p><p>${args.appName} published your ${month} owner statement for ${listingName}. Open it with the button below. You don't need a login.</p>`,\n buttonText: 'View statement',\n bodyTwo: `<p style=\"color:#737373;font-size:13px\">If the button doesn't work, copy this link into your browser: ${ownerStatementHref}</p>`,\n href: args.ownerStatementHref,\n };\n }\n\n return {\n subject: 'Owner statement was published',\n shortDescription: `An owner statement is ready for you. Log in to ${args.appName} now to view it.`,\n header: 'Owner statement was published',\n bodyOne: `<p>Hi ${userName},<p>Your ${month} owner statement for property ${listingName} is ready for your review on ${args.appName}.</p><p>To access your statement, please click the button below:</p>`,\n buttonText: 'View Owner statement',\n bodyTwo: `<p>If you have any trouble with the button, you can copy and paste the link below into your browser: ${ownerStatementHref}</p>`,\n href: args.ownerStatementHref,\n };\n};\n\nconst mutedColor = '#737373';\nconst borderColor = '#e5e7eb';\n\nexport const OwnerStatementEmailTemplate = ({\n data,\n tenant,\n themeColors,\n mode,\n user,\n listing,\n month,\n unsubscribeHref,\n renewHref,\n}: ActionEmailTemplateProps & {\n /** Requests a new no-login link once the emailed one has expired. */\n renewHref?: string;\n}) => {\n const parse = (html: string) =>\n parseEmailHtml(html, {\n href: data.href,\n userName: user.firstName || '',\n date: month || '',\n listingName: listing || '',\n });\n\n return (\n <EmailContainer\n title={data.subject}\n shortDescription={data.shortDescription}\n tenant={tenant}\n mode={mode}\n unsubscribeHref={unsubscribeHref}\n >\n <Text\n style={{\n color: themeColors[8],\n fontSize: 12,\n fontWeight: 600,\n letterSpacing: '0.08em',\n textTransform: 'uppercase',\n margin: '0 0 8px',\n }}\n >\n Owner statement\n </Text>\n <Heading\n style={{\n fontSize: 26,\n fontWeight: 600,\n lineHeight: '34px',\n margin: '0 0 32px',\n }}\n >\n {data.header?.trim() || 'Your owner statement is ready'}\n </Heading>\n\n {data.banner?.trim() && (\n <Text\n style={{\n padding: '16px 20px',\n backgroundColor: '#f8f9fa',\n borderLeft: `3px solid ${themeColors[8]}`,\n borderRadius: '4px',\n margin: '0 0 32px',\n whiteSpace: 'pre-line',\n }}\n >\n {data.banner.trim()}\n </Text>\n )}\n\n <div\n // biome-ignore lint/security/noDangerouslySetInnerHtml: values are escaped by parseEmailHtml\n dangerouslySetInnerHTML={{ __html: parse(data.bodyOne) }}\n />\n\n {(listing || month) && (\n <table\n role=\"presentation\"\n cellPadding={0}\n cellSpacing={0}\n width=\"100%\"\n style={{\n border: `1px solid ${borderColor}`,\n borderRadius: '8px',\n borderCollapse: 'separate',\n margin: '24px 0 0',\n }}\n >\n <tbody>\n {[\n ['Property', listing],\n ['Statement', month],\n ].map(([label, value], index) =>\n value ? (\n <tr key={label}>\n <td\n style={{\n padding: '14px 20px',\n color: mutedColor,\n fontSize: 14,\n borderTop: index ? `1px solid ${borderColor}` : 'none',\n }}\n >\n {label}\n </td>\n <td\n style={{\n padding: '14px 20px',\n fontSize: 14,\n fontWeight: 600,\n textAlign: 'right',\n borderTop: index ? `1px solid ${borderColor}` : 'none',\n }}\n >\n {value}\n </td>\n </tr>\n ) : null\n )}\n </tbody>\n </table>\n )}\n\n <Button\n href={data.href}\n mode={mode}\n data-testid=\"view-owner-statement-button\"\n label={data.buttonText?.trim() || 'View statement'}\n themeColors={themeColors}\n style={{ marginTop: '24px', marginBottom: '32px' }}\n />\n\n {renewHref && (\n <div\n style={{\n padding: '20px',\n backgroundColor: '#f8f9fa',\n borderRadius: '8px',\n marginBottom: '32px',\n }}\n >\n <Text style={{ fontWeight: 600, margin: '0 0 4px' }}>\n Link expired?\n </Text>\n <Text style={{ color: mutedColor, fontSize: 14, margin: 0 }}>\n For your security, this link opens only this statement and works for\n 7 days. After that, request a new one and we'll email it to this\n address.\n </Text>\n <Button\n href={renewHref}\n mode={mode}\n data-testid=\"renew-owner-statement-link-button\"\n label=\"Send me a new link\"\n themeColors={themeColors}\n style={{\n background: '#ffffff',\n border: `1px solid ${themeColors[8]}`,\n color: themeColors[8],\n marginTop: '16px',\n marginBottom: 0,\n }}\n />\n </div>\n )}\n\n <div\n // biome-ignore lint/security/noDangerouslySetInnerHtml: values are escaped by parseEmailHtml\n dangerouslySetInnerHTML={{ __html: parse(data.bodyTwo) }}\n />\n </EmailContainer>\n );\n};\n"]}
|
package/build/module/types.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"src/","sources":["types.ts"],"names":[],"mappings":"","sourcesContent":["export type EmailBaseProps = {\n mode: 'edit' | 'preview' | 'send';\n tenant: {\n name: string;\n showPlatformBranding?: boolean;\n logo: string | undefined | null;\n };\n listing: string | undefined;\n month: string | undefined;\n user: {\n firstName: string | undefined;\n };\n themeColors: readonly [\n string,\n string,\n string,\n string,\n string,\n string,\n string,\n string,\n string,\n string,\n ...string[],\n ];\n};\n"]}
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"src/","sources":["types.ts"],"names":[],"mappings":"","sourcesContent":["export type EmailBaseProps = {\n mode: 'edit' | 'preview' | 'send';\n tenant: {\n name: string;\n showPlatformBranding?: boolean;\n logo: string | undefined | null;\n };\n listing: string | undefined;\n month: string | undefined;\n /** Opt-out link shown below the email body */\n unsubscribeHref?: string;\n user: {\n firstName: string | undefined;\n };\n themeColors: readonly [\n string,\n string,\n string,\n string,\n string,\n string,\n string,\n string,\n string,\n string,\n ...string[],\n ];\n};\n"]}
|
|
@@ -29,6 +29,7 @@ export declare function sendEmail(data: {
|
|
|
29
29
|
tag?: string;
|
|
30
30
|
metadata?: Record<string, string>;
|
|
31
31
|
from?: string;
|
|
32
|
+
headers?: Record<string, string>;
|
|
32
33
|
}, options: {
|
|
33
34
|
fetch?: (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
34
35
|
postmarkToken?: string;
|
|
@@ -61,6 +61,12 @@ export async function sendEmail(data, options) {
|
|
|
61
61
|
MessageStream: 'outbound',
|
|
62
62
|
Tag: data.tag,
|
|
63
63
|
Metadata: data.metadata,
|
|
64
|
+
Headers: data.headers
|
|
65
|
+
? Object.entries(data.headers).map(([Name, Value]) => ({
|
|
66
|
+
Name,
|
|
67
|
+
Value,
|
|
68
|
+
}))
|
|
69
|
+
: undefined,
|
|
64
70
|
}),
|
|
65
71
|
});
|
|
66
72
|
const providerResponse = await response.json().catch(() => ({}));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"send-email.js","sourceRoot":"src/","sources":["utils/send-email.ts"],"names":[],"mappings":"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","sourcesContent":["import { day } from '@vrplatform/utils';\nimport { z } from 'zod';\n\nconst postmarkErrorCodeSchema = z.number().int().nonnegative();\nconst postmarkResponseErrorCodeSchema = z\n .object({ ErrorCode: postmarkErrorCodeSchema })\n .transform(({ ErrorCode }) => ErrorCode);\nconst postmarkHeaderErrorCodeSchema = z\n .string()\n .regex(/^\\d+$/)\n .transform(Number)\n .pipe(postmarkErrorCodeSchema);\nconst postmarkSuccessResponseSchema = z.object({\n ErrorCode: z.literal(0),\n MessageID: z.string().min(1),\n SubmittedAt: z.string().refine((value) => day(value).isValid()),\n});\n\nfunction parseOptional<T>(schema: z.ZodType<T>, value: unknown) {\n const result = schema.safeParse(value);\n if (result.success) return result.data;\n}\n\ntype EmailDeliveryFailureDetails = {\n httpStatus: number;\n postmarkErrorCode?: number;\n provider: 'postmark';\n providerCategory: 'inactiveRecipient' | 'rejected';\n};\n\ntype EmailDeliveryErrorDetails =\n | EmailDeliveryFailureDetails\n | {\n failureCount: number;\n failures: EmailDeliveryFailureDetails[];\n };\n\nexport class EmailDeliveryError extends Error {\n constructor(\n message: string,\n public readonly details?: EmailDeliveryErrorDetails,\n public readonly expected = false,\n public readonly retryable = true\n ) {\n super(message);\n this.name = 'EmailDeliveryError';\n }\n}\n\n/**\n * Formats a verified sender address with a display name for the From header.\n * Registrations store bare addresses; recipients should still see the company\n * name the way they see `VRPlatform <notifications@vrplatform.app>`.\n */\nexport function formatSenderIdentity(\n name: string | null | undefined,\n address: string\n) {\n const display = (name ?? '').replace(/[\\r\\n\"<>]/g, '').trim();\n return display ? `\"${display}\" <${address}>` : address;\n}\n\nexport async function sendEmail(\n data: {\n to: string;\n subject: string;\n text: string;\n html: string;\n replyTo?: string;\n tag?: string;\n metadata?: Record<string, string>;\n from?: string;\n },\n options: {\n fetch?: (\n input: string | URL | Request,\n init?: RequestInit\n ) => Promise<Response>;\n postmarkToken?: string;\n }\n) {\n if (!options.postmarkToken) {\n throw new EmailDeliveryError(\n 'Postmark is not configured',\n undefined,\n false,\n false\n );\n }\n\n const response = await (options.fetch ?? fetch)(\n 'https://api.postmarkapp.com/email',\n {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n 'X-Postmark-Server-Token': options.postmarkToken,\n },\n body: JSON.stringify({\n From: data.from ?? 'VRPlatform <notifications@vrplatform.app>',\n To: data.to,\n Subject: data.subject,\n ReplyTo: data.replyTo,\n TextBody: data.text,\n HtmlBody: data.html,\n MessageStream: 'outbound',\n Tag: data.tag,\n Metadata: data.metadata,\n }),\n }\n );\n\n const providerResponse: unknown = await response.json().catch(() => ({}));\n const postmarkErrorCode =\n parseOptional(postmarkResponseErrorCodeSchema, providerResponse) ??\n parseOptional(\n postmarkHeaderErrorCodeSchema,\n response.headers.get('X-PM-ApiErrorCode')\n );\n\n if (\n !response.ok ||\n (postmarkErrorCode !== undefined && postmarkErrorCode !== 0)\n ) {\n const inactiveRecipient =\n response.status === 422 && postmarkErrorCode === 406;\n const details: EmailDeliveryFailureDetails = {\n provider: 'postmark',\n providerCategory: inactiveRecipient ? 'inactiveRecipient' : 'rejected',\n httpStatus: response.status,\n ...(postmarkErrorCode !== undefined ? { postmarkErrorCode } : {}),\n };\n\n if (inactiveRecipient) {\n throw new EmailDeliveryError(\n 'Postmark recipient is inactive',\n details,\n true,\n false\n );\n }\n\n throw new EmailDeliveryError(\n 'Postmark rejected the email',\n details,\n false,\n response.status === 429 || response.status >= 500\n );\n }\n\n const result = postmarkSuccessResponseSchema.safeParse(providerResponse);\n if (!result.success) {\n throw new EmailDeliveryError('Postmark returned an invalid response', {\n provider: 'postmark',\n providerCategory: 'rejected',\n httpStatus: response.status,\n ...(postmarkErrorCode !== undefined ? { postmarkErrorCode } : {}),\n });\n }\n\n return {\n httpStatus: response.status,\n messageId: result.data.MessageID,\n submittedAt: result.data.SubmittedAt,\n };\n}\n\nexport async function settleEmailDeliveries(deliveries: Promise<unknown>[]) {\n const failures = (await Promise.allSettled(deliveries)).filter(\n (result) => result.status === 'rejected'\n );\n if (failures.length === 0) return;\n\n const unexpectedDeliveryError = failures.find(\n (failure) =>\n failure.reason instanceof EmailDeliveryError && !failure.reason.expected\n )?.reason;\n const allExpected = failures.every(\n (failure) =>\n failure.reason instanceof EmailDeliveryError && failure.reason.expected\n );\n const firstDeliveryError = failures.find(\n (failure) => failure.reason instanceof EmailDeliveryError\n )?.reason;\n const failureDetails = failures.flatMap((failure) => {\n if (\n !(failure.reason instanceof EmailDeliveryError) ||\n !failure.reason.details ||\n !('provider' in failure.reason.details)\n ) {\n return [];\n }\n const { httpStatus, postmarkErrorCode, provider, providerCategory } =\n failure.reason.details;\n return [\n {\n httpStatus,\n provider,\n providerCategory,\n ...(postmarkErrorCode !== undefined ? { postmarkErrorCode } : {}),\n },\n ];\n });\n\n const details: EmailDeliveryErrorDetails = {\n failureCount: failures.length,\n failures: failureDetails,\n };\n if (unexpectedDeliveryError instanceof EmailDeliveryError) {\n if (unexpectedDeliveryError.message === 'Postmark is not configured') {\n throw new EmailDeliveryError(\n 'Postmark is not configured',\n details,\n false,\n false\n );\n }\n throw new EmailDeliveryError(\n 'Postmark rejected the email',\n details,\n false,\n unexpectedDeliveryError.retryable\n );\n }\n if (allExpected && firstDeliveryError instanceof EmailDeliveryError) {\n throw new EmailDeliveryError(\n 'Postmark recipient is inactive',\n details,\n true,\n false\n );\n }\n throw new EmailDeliveryError('Email delivery failed', details);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"send-email.js","sourceRoot":"src/","sources":["utils/send-email.ts"],"names":[],"mappings":"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","sourcesContent":["import { day } from '@vrplatform/utils';\nimport { z } from 'zod';\n\nconst postmarkErrorCodeSchema = z.number().int().nonnegative();\nconst postmarkResponseErrorCodeSchema = z\n .object({ ErrorCode: postmarkErrorCodeSchema })\n .transform(({ ErrorCode }) => ErrorCode);\nconst postmarkHeaderErrorCodeSchema = z\n .string()\n .regex(/^\\d+$/)\n .transform(Number)\n .pipe(postmarkErrorCodeSchema);\nconst postmarkSuccessResponseSchema = z.object({\n ErrorCode: z.literal(0),\n MessageID: z.string().min(1),\n SubmittedAt: z.string().refine((value) => day(value).isValid()),\n});\n\nfunction parseOptional<T>(schema: z.ZodType<T>, value: unknown) {\n const result = schema.safeParse(value);\n if (result.success) return result.data;\n}\n\ntype EmailDeliveryFailureDetails = {\n httpStatus: number;\n postmarkErrorCode?: number;\n provider: 'postmark';\n providerCategory: 'inactiveRecipient' | 'rejected';\n};\n\ntype EmailDeliveryErrorDetails =\n | EmailDeliveryFailureDetails\n | {\n failureCount: number;\n failures: EmailDeliveryFailureDetails[];\n };\n\nexport class EmailDeliveryError extends Error {\n constructor(\n message: string,\n public readonly details?: EmailDeliveryErrorDetails,\n public readonly expected = false,\n public readonly retryable = true\n ) {\n super(message);\n this.name = 'EmailDeliveryError';\n }\n}\n\n/**\n * Formats a verified sender address with a display name for the From header.\n * Registrations store bare addresses; recipients should still see the company\n * name the way they see `VRPlatform <notifications@vrplatform.app>`.\n */\nexport function formatSenderIdentity(\n name: string | null | undefined,\n address: string\n) {\n const display = (name ?? '').replace(/[\\r\\n\"<>]/g, '').trim();\n return display ? `\"${display}\" <${address}>` : address;\n}\n\nexport async function sendEmail(\n data: {\n to: string;\n subject: string;\n text: string;\n html: string;\n replyTo?: string;\n tag?: string;\n metadata?: Record<string, string>;\n from?: string;\n headers?: Record<string, string>;\n },\n options: {\n fetch?: (\n input: string | URL | Request,\n init?: RequestInit\n ) => Promise<Response>;\n postmarkToken?: string;\n }\n) {\n if (!options.postmarkToken) {\n throw new EmailDeliveryError(\n 'Postmark is not configured',\n undefined,\n false,\n false\n );\n }\n\n const response = await (options.fetch ?? fetch)(\n 'https://api.postmarkapp.com/email',\n {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n 'X-Postmark-Server-Token': options.postmarkToken,\n },\n body: JSON.stringify({\n From: data.from ?? 'VRPlatform <notifications@vrplatform.app>',\n To: data.to,\n Subject: data.subject,\n ReplyTo: data.replyTo,\n TextBody: data.text,\n HtmlBody: data.html,\n MessageStream: 'outbound',\n Tag: data.tag,\n Metadata: data.metadata,\n Headers: data.headers\n ? Object.entries(data.headers).map(([Name, Value]) => ({\n Name,\n Value,\n }))\n : undefined,\n }),\n }\n );\n\n const providerResponse: unknown = await response.json().catch(() => ({}));\n const postmarkErrorCode =\n parseOptional(postmarkResponseErrorCodeSchema, providerResponse) ??\n parseOptional(\n postmarkHeaderErrorCodeSchema,\n response.headers.get('X-PM-ApiErrorCode')\n );\n\n if (\n !response.ok ||\n (postmarkErrorCode !== undefined && postmarkErrorCode !== 0)\n ) {\n const inactiveRecipient =\n response.status === 422 && postmarkErrorCode === 406;\n const details: EmailDeliveryFailureDetails = {\n provider: 'postmark',\n providerCategory: inactiveRecipient ? 'inactiveRecipient' : 'rejected',\n httpStatus: response.status,\n ...(postmarkErrorCode !== undefined ? { postmarkErrorCode } : {}),\n };\n\n if (inactiveRecipient) {\n throw new EmailDeliveryError(\n 'Postmark recipient is inactive',\n details,\n true,\n false\n );\n }\n\n throw new EmailDeliveryError(\n 'Postmark rejected the email',\n details,\n false,\n response.status === 429 || response.status >= 500\n );\n }\n\n const result = postmarkSuccessResponseSchema.safeParse(providerResponse);\n if (!result.success) {\n throw new EmailDeliveryError('Postmark returned an invalid response', {\n provider: 'postmark',\n providerCategory: 'rejected',\n httpStatus: response.status,\n ...(postmarkErrorCode !== undefined ? { postmarkErrorCode } : {}),\n });\n }\n\n return {\n httpStatus: response.status,\n messageId: result.data.MessageID,\n submittedAt: result.data.SubmittedAt,\n };\n}\n\nexport async function settleEmailDeliveries(deliveries: Promise<unknown>[]) {\n const failures = (await Promise.allSettled(deliveries)).filter(\n (result) => result.status === 'rejected'\n );\n if (failures.length === 0) return;\n\n const unexpectedDeliveryError = failures.find(\n (failure) =>\n failure.reason instanceof EmailDeliveryError && !failure.reason.expected\n )?.reason;\n const allExpected = failures.every(\n (failure) =>\n failure.reason instanceof EmailDeliveryError && failure.reason.expected\n );\n const firstDeliveryError = failures.find(\n (failure) => failure.reason instanceof EmailDeliveryError\n )?.reason;\n const failureDetails = failures.flatMap((failure) => {\n if (\n !(failure.reason instanceof EmailDeliveryError) ||\n !failure.reason.details ||\n !('provider' in failure.reason.details)\n ) {\n return [];\n }\n const { httpStatus, postmarkErrorCode, provider, providerCategory } =\n failure.reason.details;\n return [\n {\n httpStatus,\n provider,\n providerCategory,\n ...(postmarkErrorCode !== undefined ? { postmarkErrorCode } : {}),\n },\n ];\n });\n\n const details: EmailDeliveryErrorDetails = {\n failureCount: failures.length,\n failures: failureDetails,\n };\n if (unexpectedDeliveryError instanceof EmailDeliveryError) {\n if (unexpectedDeliveryError.message === 'Postmark is not configured') {\n throw new EmailDeliveryError(\n 'Postmark is not configured',\n details,\n false,\n false\n );\n }\n throw new EmailDeliveryError(\n 'Postmark rejected the email',\n details,\n false,\n unexpectedDeliveryError.retryable\n );\n }\n if (allExpected && firstDeliveryError instanceof EmailDeliveryError) {\n throw new EmailDeliveryError(\n 'Postmark recipient is inactive',\n details,\n true,\n false\n );\n }\n throw new EmailDeliveryError('Email delivery failed', details);\n}\n"]}
|
package/package.json
CHANGED
|
@@ -28,6 +28,7 @@ export const ActionEmailTemplate = ({
|
|
|
28
28
|
user,
|
|
29
29
|
listing,
|
|
30
30
|
month,
|
|
31
|
+
unsubscribeHref,
|
|
31
32
|
}: ActionEmailTemplateProps) => {
|
|
32
33
|
return (
|
|
33
34
|
<EmailContainer
|
|
@@ -35,6 +36,7 @@ export const ActionEmailTemplate = ({
|
|
|
35
36
|
shortDescription={data.shortDescription}
|
|
36
37
|
tenant={tenant}
|
|
37
38
|
mode={mode}
|
|
39
|
+
unsubscribeHref={unsubscribeHref}
|
|
38
40
|
>
|
|
39
41
|
<Heading
|
|
40
42
|
style={{
|
|
@@ -15,6 +15,7 @@ type Props = {
|
|
|
15
15
|
tenant: Omit<EmailBaseProps['tenant'], 'name'> & { name?: string };
|
|
16
16
|
mode: EmailBaseProps['mode'];
|
|
17
17
|
shortDescription?: string;
|
|
18
|
+
unsubscribeHref?: string;
|
|
18
19
|
};
|
|
19
20
|
|
|
20
21
|
export const EmailContainer = (props: Props) => {
|
|
@@ -72,6 +73,7 @@ const Content = ({
|
|
|
72
73
|
children,
|
|
73
74
|
tenant: { logo: tenantLogo, name, showPlatformBranding = true },
|
|
74
75
|
mode,
|
|
76
|
+
unsubscribeHref,
|
|
75
77
|
}: Props) => {
|
|
76
78
|
return (
|
|
77
79
|
<>
|
|
@@ -112,6 +114,13 @@ const Content = ({
|
|
|
112
114
|
{children}
|
|
113
115
|
|
|
114
116
|
{showPlatformBranding ? <Footer /> : <Text>{name}</Text>}
|
|
117
|
+
{unsubscribeHref && (
|
|
118
|
+
<Text style={{ color: '#737373' }}>
|
|
119
|
+
<a href={unsubscribeHref} style={{ color: '#737373' }}>
|
|
120
|
+
Unsubscribe from owner statement emails
|
|
121
|
+
</a>
|
|
122
|
+
</Text>
|
|
123
|
+
)}
|
|
115
124
|
</>
|
|
116
125
|
);
|
|
117
126
|
};
|
|
@@ -1,14 +1,18 @@
|
|
|
1
|
+
import { Heading } from '@react-email/heading';
|
|
2
|
+
import { Text } from '@react-email/text';
|
|
1
3
|
import {
|
|
2
4
|
EMAIL_ACTION_LINK_COMPONENT,
|
|
3
5
|
EMAIL_DATE_COMPONENT,
|
|
4
6
|
EMAIL_LISTING_COMPONENT,
|
|
5
7
|
EMAIL_USER_NAME_COMPONENT,
|
|
6
8
|
} from '../extensions';
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
type ActionEmailTemplateProps,
|
|
9
|
+
import type {
|
|
10
|
+
ActionEmailData,
|
|
11
|
+
ActionEmailTemplateProps,
|
|
11
12
|
} from '../components/ActionEmailTemplate';
|
|
13
|
+
import { Button } from '../components/Button';
|
|
14
|
+
import { EmailContainer } from '../components/EmailContainer';
|
|
15
|
+
import { parseEmailHtml } from '../components/utils';
|
|
12
16
|
|
|
13
17
|
export const getDefaultOwnerStatementEmailData = (
|
|
14
18
|
mode: 'tag' | 'text',
|
|
@@ -18,6 +22,8 @@ export const getDefaultOwnerStatementEmailData = (
|
|
|
18
22
|
listingName: string;
|
|
19
23
|
userName: string;
|
|
20
24
|
month: string;
|
|
25
|
+
/** The owner opens the statement through an emailed link, without a login. */
|
|
26
|
+
noLogin?: boolean;
|
|
21
27
|
}
|
|
22
28
|
): ActionEmailData => {
|
|
23
29
|
const ownerStatementHref =
|
|
@@ -28,6 +34,26 @@ export const getDefaultOwnerStatementEmailData = (
|
|
|
28
34
|
const month = mode === 'tag' ? EMAIL_DATE_COMPONENT : args.month;
|
|
29
35
|
const userName = mode === 'tag' ? EMAIL_USER_NAME_COMPONENT : args.userName;
|
|
30
36
|
|
|
37
|
+
if (args.noLogin) {
|
|
38
|
+
// Subject and preview text are plain text without tokens, so a saved
|
|
39
|
+
// template must not keep the editor's sample month and listing.
|
|
40
|
+
return {
|
|
41
|
+
subject:
|
|
42
|
+
mode === 'tag'
|
|
43
|
+
? 'Your owner statement is ready'
|
|
44
|
+
: `Your ${args.month} owner statement for ${args.listingName}`,
|
|
45
|
+
shortDescription:
|
|
46
|
+
mode === 'tag'
|
|
47
|
+
? 'Your owner statement is ready. Open it with one click, no login needed.'
|
|
48
|
+
: `Your ${args.month} statement for ${args.listingName} is ready. Open it with one click, no login needed.`,
|
|
49
|
+
header: 'Your owner statement is ready',
|
|
50
|
+
bodyOne: `<p>Hi ${userName},</p><p>${args.appName} published your ${month} owner statement for ${listingName}. Open it with the button below. You don't need a login.</p>`,
|
|
51
|
+
buttonText: 'View statement',
|
|
52
|
+
bodyTwo: `<p style="color:#737373;font-size:13px">If the button doesn't work, copy this link into your browser: ${ownerStatementHref}</p>`,
|
|
53
|
+
href: args.ownerStatementHref,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
31
57
|
return {
|
|
32
58
|
subject: 'Owner statement was published',
|
|
33
59
|
shortDescription: `An owner statement is ready for you. Log in to ${args.appName} now to view it.`,
|
|
@@ -39,8 +65,177 @@ export const getDefaultOwnerStatementEmailData = (
|
|
|
39
65
|
};
|
|
40
66
|
};
|
|
41
67
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
68
|
+
const mutedColor = '#737373';
|
|
69
|
+
const borderColor = '#e5e7eb';
|
|
70
|
+
|
|
71
|
+
export const OwnerStatementEmailTemplate = ({
|
|
72
|
+
data,
|
|
73
|
+
tenant,
|
|
74
|
+
themeColors,
|
|
75
|
+
mode,
|
|
76
|
+
user,
|
|
77
|
+
listing,
|
|
78
|
+
month,
|
|
79
|
+
unsubscribeHref,
|
|
80
|
+
renewHref,
|
|
81
|
+
}: ActionEmailTemplateProps & {
|
|
82
|
+
/** Requests a new no-login link once the emailed one has expired. */
|
|
83
|
+
renewHref?: string;
|
|
84
|
+
}) => {
|
|
85
|
+
const parse = (html: string) =>
|
|
86
|
+
parseEmailHtml(html, {
|
|
87
|
+
href: data.href,
|
|
88
|
+
userName: user.firstName || '',
|
|
89
|
+
date: month || '',
|
|
90
|
+
listingName: listing || '',
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<EmailContainer
|
|
95
|
+
title={data.subject}
|
|
96
|
+
shortDescription={data.shortDescription}
|
|
97
|
+
tenant={tenant}
|
|
98
|
+
mode={mode}
|
|
99
|
+
unsubscribeHref={unsubscribeHref}
|
|
100
|
+
>
|
|
101
|
+
<Text
|
|
102
|
+
style={{
|
|
103
|
+
color: themeColors[8],
|
|
104
|
+
fontSize: 12,
|
|
105
|
+
fontWeight: 600,
|
|
106
|
+
letterSpacing: '0.08em',
|
|
107
|
+
textTransform: 'uppercase',
|
|
108
|
+
margin: '0 0 8px',
|
|
109
|
+
}}
|
|
110
|
+
>
|
|
111
|
+
Owner statement
|
|
112
|
+
</Text>
|
|
113
|
+
<Heading
|
|
114
|
+
style={{
|
|
115
|
+
fontSize: 26,
|
|
116
|
+
fontWeight: 600,
|
|
117
|
+
lineHeight: '34px',
|
|
118
|
+
margin: '0 0 32px',
|
|
119
|
+
}}
|
|
120
|
+
>
|
|
121
|
+
{data.header?.trim() || 'Your owner statement is ready'}
|
|
122
|
+
</Heading>
|
|
123
|
+
|
|
124
|
+
{data.banner?.trim() && (
|
|
125
|
+
<Text
|
|
126
|
+
style={{
|
|
127
|
+
padding: '16px 20px',
|
|
128
|
+
backgroundColor: '#f8f9fa',
|
|
129
|
+
borderLeft: `3px solid ${themeColors[8]}`,
|
|
130
|
+
borderRadius: '4px',
|
|
131
|
+
margin: '0 0 32px',
|
|
132
|
+
whiteSpace: 'pre-line',
|
|
133
|
+
}}
|
|
134
|
+
>
|
|
135
|
+
{data.banner.trim()}
|
|
136
|
+
</Text>
|
|
137
|
+
)}
|
|
138
|
+
|
|
139
|
+
<div
|
|
140
|
+
// biome-ignore lint/security/noDangerouslySetInnerHtml: values are escaped by parseEmailHtml
|
|
141
|
+
dangerouslySetInnerHTML={{ __html: parse(data.bodyOne) }}
|
|
142
|
+
/>
|
|
143
|
+
|
|
144
|
+
{(listing || month) && (
|
|
145
|
+
<table
|
|
146
|
+
role="presentation"
|
|
147
|
+
cellPadding={0}
|
|
148
|
+
cellSpacing={0}
|
|
149
|
+
width="100%"
|
|
150
|
+
style={{
|
|
151
|
+
border: `1px solid ${borderColor}`,
|
|
152
|
+
borderRadius: '8px',
|
|
153
|
+
borderCollapse: 'separate',
|
|
154
|
+
margin: '24px 0 0',
|
|
155
|
+
}}
|
|
156
|
+
>
|
|
157
|
+
<tbody>
|
|
158
|
+
{[
|
|
159
|
+
['Property', listing],
|
|
160
|
+
['Statement', month],
|
|
161
|
+
].map(([label, value], index) =>
|
|
162
|
+
value ? (
|
|
163
|
+
<tr key={label}>
|
|
164
|
+
<td
|
|
165
|
+
style={{
|
|
166
|
+
padding: '14px 20px',
|
|
167
|
+
color: mutedColor,
|
|
168
|
+
fontSize: 14,
|
|
169
|
+
borderTop: index ? `1px solid ${borderColor}` : 'none',
|
|
170
|
+
}}
|
|
171
|
+
>
|
|
172
|
+
{label}
|
|
173
|
+
</td>
|
|
174
|
+
<td
|
|
175
|
+
style={{
|
|
176
|
+
padding: '14px 20px',
|
|
177
|
+
fontSize: 14,
|
|
178
|
+
fontWeight: 600,
|
|
179
|
+
textAlign: 'right',
|
|
180
|
+
borderTop: index ? `1px solid ${borderColor}` : 'none',
|
|
181
|
+
}}
|
|
182
|
+
>
|
|
183
|
+
{value}
|
|
184
|
+
</td>
|
|
185
|
+
</tr>
|
|
186
|
+
) : null
|
|
187
|
+
)}
|
|
188
|
+
</tbody>
|
|
189
|
+
</table>
|
|
190
|
+
)}
|
|
191
|
+
|
|
192
|
+
<Button
|
|
193
|
+
href={data.href}
|
|
194
|
+
mode={mode}
|
|
195
|
+
data-testid="view-owner-statement-button"
|
|
196
|
+
label={data.buttonText?.trim() || 'View statement'}
|
|
197
|
+
themeColors={themeColors}
|
|
198
|
+
style={{ marginTop: '24px', marginBottom: '32px' }}
|
|
199
|
+
/>
|
|
200
|
+
|
|
201
|
+
{renewHref && (
|
|
202
|
+
<div
|
|
203
|
+
style={{
|
|
204
|
+
padding: '20px',
|
|
205
|
+
backgroundColor: '#f8f9fa',
|
|
206
|
+
borderRadius: '8px',
|
|
207
|
+
marginBottom: '32px',
|
|
208
|
+
}}
|
|
209
|
+
>
|
|
210
|
+
<Text style={{ fontWeight: 600, margin: '0 0 4px' }}>
|
|
211
|
+
Link expired?
|
|
212
|
+
</Text>
|
|
213
|
+
<Text style={{ color: mutedColor, fontSize: 14, margin: 0 }}>
|
|
214
|
+
For your security, this link opens only this statement and works for
|
|
215
|
+
7 days. After that, request a new one and we'll email it to this
|
|
216
|
+
address.
|
|
217
|
+
</Text>
|
|
218
|
+
<Button
|
|
219
|
+
href={renewHref}
|
|
220
|
+
mode={mode}
|
|
221
|
+
data-testid="renew-owner-statement-link-button"
|
|
222
|
+
label="Send me a new link"
|
|
223
|
+
themeColors={themeColors}
|
|
224
|
+
style={{
|
|
225
|
+
background: '#ffffff',
|
|
226
|
+
border: `1px solid ${themeColors[8]}`,
|
|
227
|
+
color: themeColors[8],
|
|
228
|
+
marginTop: '16px',
|
|
229
|
+
marginBottom: 0,
|
|
230
|
+
}}
|
|
231
|
+
/>
|
|
232
|
+
</div>
|
|
233
|
+
)}
|
|
234
|
+
|
|
235
|
+
<div
|
|
236
|
+
// biome-ignore lint/security/noDangerouslySetInnerHtml: values are escaped by parseEmailHtml
|
|
237
|
+
dangerouslySetInnerHTML={{ __html: parse(data.bodyTwo) }}
|
|
238
|
+
/>
|
|
239
|
+
</EmailContainer>
|
|
240
|
+
);
|
|
46
241
|
};
|
package/src/types.ts
CHANGED
package/src/utils/send-email.ts
CHANGED
|
@@ -70,6 +70,7 @@ export async function sendEmail(
|
|
|
70
70
|
tag?: string;
|
|
71
71
|
metadata?: Record<string, string>;
|
|
72
72
|
from?: string;
|
|
73
|
+
headers?: Record<string, string>;
|
|
73
74
|
},
|
|
74
75
|
options: {
|
|
75
76
|
fetch?: (
|
|
@@ -107,6 +108,12 @@ export async function sendEmail(
|
|
|
107
108
|
MessageStream: 'outbound',
|
|
108
109
|
Tag: data.tag,
|
|
109
110
|
Metadata: data.metadata,
|
|
111
|
+
Headers: data.headers
|
|
112
|
+
? Object.entries(data.headers).map(([Name, Value]) => ({
|
|
113
|
+
Name,
|
|
114
|
+
Value,
|
|
115
|
+
}))
|
|
116
|
+
: undefined,
|
|
110
117
|
}),
|
|
111
118
|
}
|
|
112
119
|
);
|