@vrplatform/email 1.0.0 → 1.0.2

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.
@@ -0,0 +1,11 @@
1
+ import type { EmailBaseProps } from '../types';
2
+ type NotificationKey = '$ownerStatementInReview' | '$ownerStatementPublished' | '$inviteOwnerAccepted' | '$inviteTeamMemberAccepted' | '$1099StatementPublished';
3
+ export type DailyDigestEmailTemplateProps = {
4
+ mode: EmailBaseProps['mode'];
5
+ unreadNotifications: Record<NotificationKey, {
6
+ id: string;
7
+ name: string;
8
+ }[]>;
9
+ };
10
+ export declare const DailyDigestEmailTemplate: ({ mode, unreadNotifications, }: DailyDigestEmailTemplateProps) => import("react/jsx-runtime").JSX.Element;
11
+ export {};
@@ -0,0 +1,97 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Heading } from '@react-email/heading';
3
+ import { Link } from '@react-email/link';
4
+ import { Text } from '@react-email/text';
5
+ import { EmailContainer } from '../components';
6
+ import { Button } from '../components/Button';
7
+ const THEME_VRP = [
8
+ // baseColor: #012F64
9
+ '#E6F1FF',
10
+ '#B8D9FE',
11
+ '#8BC0FE',
12
+ '#5DA8FD',
13
+ '#308FFD',
14
+ '#0377FC',
15
+ '#025FCA',
16
+ '#024797',
17
+ '#012F65',
18
+ '#011832',
19
+ ];
20
+ const THEME_NEUTRAL = [
21
+ '#fafafa',
22
+ '#f5f5f5',
23
+ '#e5e5e5',
24
+ '#d4d4d4',
25
+ '#a3a3a3',
26
+ '#737373',
27
+ '#525252',
28
+ '#404040',
29
+ '#262626',
30
+ '#171717',
31
+ // "#0a0a0a"
32
+ ];
33
+ const NOTIFICATION_ORDER = {
34
+ $inviteOwnerAccepted: {
35
+ title: 'Owners invitations',
36
+ text: 'accepted invite',
37
+ order: 1,
38
+ },
39
+ $inviteTeamMemberAccepted: {
40
+ title: 'Team member invitations',
41
+ text: 'accepted invite',
42
+ order: 2,
43
+ },
44
+ $ownerStatementInReview: {
45
+ title: 'Owner statements ready for review',
46
+ text: 'is ready for review',
47
+ order: 3,
48
+ },
49
+ $ownerStatementPublished: {
50
+ title: 'Owner statements were published',
51
+ text: 'was published to owner portal',
52
+ order: 4,
53
+ },
54
+ $1099StatementPublished: {
55
+ title: '1099 tax statements were published',
56
+ text: 'was finalized and published',
57
+ order: 5,
58
+ },
59
+ };
60
+ export const DailyDigestEmailTemplate = ({ mode, unreadNotifications, }) => {
61
+ const total = Object.values(unreadNotifications).reduce((acc, curr) => acc + curr.length, 0);
62
+ const emailTitle = `You have ${total} unread notification${total > 1 ? 's' : ''}`;
63
+ const link = 'https://portal.vrplatform.app/dashboard/inbox';
64
+ return (_jsxs(EmailContainer, { title: emailTitle, shortDescription: "Login to VRPlatform now to check your team's activity!", tenant: {
65
+ logo: undefined,
66
+ }, mode: mode, children: [_jsx(Heading, { style: {
67
+ fontSize: 26,
68
+ fontWeight: 600,
69
+ marginBottom: '50px',
70
+ }, children: emailTitle }), Object.entries(unreadNotifications)
71
+ .sort(([aKey], [bKey]) => {
72
+ const a = NOTIFICATION_ORDER[aKey]?.order ?? 0;
73
+ const b = NOTIFICATION_ORDER[bKey]?.order ?? 0;
74
+ return a - b;
75
+ })
76
+ .map(([key, notifications]) => {
77
+ const data = NOTIFICATION_ORDER[key];
78
+ if (notifications.length === 0 || !data)
79
+ return null;
80
+ return (_jsxs("div", { style: {
81
+ marginBottom: '42px',
82
+ }, children: [_jsx(Text, { style: {
83
+ color: THEME_NEUTRAL[5],
84
+ }, children: data.title }), notifications.map((not) => (_jsxs(Text, { style: { color: THEME_NEUTRAL[5] }, children: [_jsx("span", { style: {
85
+ fontWeight: 800,
86
+ color: '#000',
87
+ }, children: not.name }), ' ', data.text] }, not.id)))] }, key));
88
+ }), _jsx(Button, { href: link, mode: mode, "data-testid": "accept-invite-button", label: 'Go to VRPlatform', themeColors: THEME_VRP, style: {
89
+ marginTop: '50px',
90
+ marginBottom: '50px',
91
+ } }), _jsx(Text, { children: "Best, VRPlatform" }), _jsxs(Text, { children: ["If you have any trouble with the button, you can copy and paste the link below into your browser:", ' ', _jsx(Link, { href: link, style: {
92
+ display: 'inline',
93
+ wordBreak: 'break-all',
94
+ color: THEME_VRP[7],
95
+ }, children: link })] })] }));
96
+ };
97
+ //# sourceMappingURL=DailyDigestEmailTemplate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DailyDigestEmailTemplate.js","sourceRoot":"src/","sources":["templates/DailyDigestEmailTemplate.tsx"],"names":[],"mappings":"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"}
@@ -5,3 +5,4 @@ export * from './OwnerStatementEmailTemplate';
5
5
  export * from './OwnerTaxStatementEmailTemplate';
6
6
  export * from './InviteOwnerEmailTemplate';
7
7
  export * from './InvitePmEmailTemplate';
8
+ export * from './DailyDigestEmailTemplate';
@@ -9,4 +9,6 @@ export * from './OwnerTaxStatementEmailTemplate';
9
9
  // Invitations
10
10
  export * from './InviteOwnerEmailTemplate';
11
11
  export * from './InvitePmEmailTemplate';
12
+ // Daily Digest
13
+ export * from './DailyDigestEmailTemplate';
12
14
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"src/","sources":["templates/index.ts"],"names":[],"mappings":"AAAA,QAAQ;AACR,cAAc,iCAAiC,CAAC;AAChD,cAAc,+BAA+B,CAAC;AAE9C,mBAAmB;AACnB,cAAc,sCAAsC,CAAC;AACrD,cAAc,+BAA+B,CAAC;AAE9C,uBAAuB;AACvB,cAAc,kCAAkC,CAAC;AAEjD,cAAc;AACd,cAAc,4BAA4B,CAAC;AAC3C,cAAc,yBAAyB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"src/","sources":["templates/index.ts"],"names":[],"mappings":"AAAA,QAAQ;AACR,cAAc,iCAAiC,CAAC;AAChD,cAAc,+BAA+B,CAAC;AAE9C,mBAAmB;AACnB,cAAc,sCAAsC,CAAC;AACrD,cAAc,+BAA+B,CAAC;AAE9C,uBAAuB;AACvB,cAAc,kCAAkC,CAAC;AAEjD,cAAc;AACd,cAAc,4BAA4B,CAAC;AAC3C,cAAc,yBAAyB,CAAC;AAExC,eAAe;AACf,cAAc,4BAA4B,CAAC"}
@@ -1,4 +1,4 @@
1
- import type { Maybe } from '@finalytic/utils';
1
+ import type { Maybe } from '@vrplatform/utils';
2
2
  export declare const getOwnerPortalColor: (color: Maybe<string>) => string | undefined;
3
3
  export declare const OWNER_PORTAL_COLORS: {
4
4
  orange: string;
@@ -1 +1 @@
1
- {"root":["../../src/const.ts","../../src/extensions.ts","../../src/index.ts","../../src/theme.ts","../../src/types.ts","../../src/components/actionemailtemplate.tsx","../../src/components/button.tsx","../../src/components/emailcontainer.tsx","../../src/components/index.ts","../../src/components/utils/index.ts","../../src/components/utils/parse-email-html.test.ts","../../src/components/utils/parse-email-html.ts","../../src/templates/inviteowneremailtemplate.tsx","../../src/templates/invitepmemailtemplate.tsx","../../src/templates/ownerstatementemailtemplate.tsx","../../src/templates/ownerstatementsummaryemailtemplate.tsx","../../src/templates/ownertaxstatementemailtemplate.tsx","../../src/templates/tenantdeletionemailtemplate.tsx","../../src/templates/tenantsuspensionemailtemplate.tsx","../../src/templates/index.ts","../../src/utils/index.ts","../../src/utils/send-email.ts"],"version":"5.8.3"}
1
+ {"root":["../../src/const.ts","../../src/extensions.ts","../../src/index.ts","../../src/liveblocks.d.ts","../../src/theme.ts","../../src/types.ts","../../src/components/actionemailtemplate.tsx","../../src/components/button.tsx","../../src/components/emailcontainer.tsx","../../src/components/index.ts","../../src/components/utils/index.ts","../../src/components/utils/parse-email-html.test.ts","../../src/components/utils/parse-email-html.ts","../../src/templates/dailydigestemailtemplate.tsx","../../src/templates/inviteowneremailtemplate.tsx","../../src/templates/invitepmemailtemplate.tsx","../../src/templates/ownerstatementemailtemplate.tsx","../../src/templates/ownerstatementsummaryemailtemplate.tsx","../../src/templates/ownertaxstatementemailtemplate.tsx","../../src/templates/tenantdeletionemailtemplate.tsx","../../src/templates/tenantsuspensionemailtemplate.tsx","../../src/templates/index.ts","../../src/utils/index.ts","../../src/utils/send-email.ts"],"version":"5.9.3"}
@@ -1,5 +1,18 @@
1
- export declare const sendEmail: ({ POSTMARK_TOKEN, data, }: {
1
+ import type { logger as log } from '@trigger.dev/sdk';
2
+ type SentryErrorOptions = {
3
+ skipSentry?: boolean;
4
+ sentryMessage?: string;
5
+ statusCode?: number;
6
+ };
7
+ export declare class SentryError extends Error {
8
+ readonly skipSentry: boolean;
9
+ readonly sentryMessage?: string;
10
+ readonly statusCode?: number;
11
+ constructor(message: string, options?: SentryErrorOptions);
12
+ }
13
+ export declare const sendEmail: ({ POSTMARK_TOKEN, data, logger, }: {
2
14
  POSTMARK_TOKEN: string;
15
+ logger: typeof log;
3
16
  data: {
4
17
  to: string;
5
18
  subject: string;
@@ -7,4 +20,5 @@ export declare const sendEmail: ({ POSTMARK_TOKEN, data, }: {
7
20
  html: string;
8
21
  replyTo?: string;
9
22
  };
10
- }) => Promise<Response>;
23
+ }) => Promise<void>;
24
+ export {};
@@ -1,4 +1,13 @@
1
- export const sendEmail = async ({ POSTMARK_TOKEN, data, }) => {
1
+ export class SentryError extends Error {
2
+ constructor(message, options = {}) {
3
+ super(message);
4
+ this.name = 'SentryError';
5
+ this.skipSentry = options.skipSentry ?? false;
6
+ this.sentryMessage = options.sentryMessage;
7
+ this.statusCode = options.statusCode;
8
+ }
9
+ }
10
+ export const sendEmail = async ({ POSTMARK_TOKEN, data, logger, }) => {
2
11
  const response = await fetch('https://api.postmarkapp.com/email', {
3
12
  method: 'POST',
4
13
  headers: {
@@ -16,9 +25,24 @@ export const sendEmail = async ({ POSTMARK_TOKEN, data, }) => {
16
25
  MessageStream: 'outbound',
17
26
  }),
18
27
  });
28
+ const json = await response.json().catch(() => ({}));
29
+ logger.info('Postmark Response', {
30
+ status: response.status,
31
+ statusText: response.statusText,
32
+ body: JSON.stringify(json, null, 2),
33
+ });
19
34
  if (!response.ok) {
20
- throw new Error(`Failed to send email: ${response.statusText}`);
35
+ if (response.status === 422 &&
36
+ 'Message' in json &&
37
+ typeof json.Message === 'string') {
38
+ throw new SentryError(json.Message, {
39
+ skipSentry: true,
40
+ statusCode: 422,
41
+ });
42
+ }
43
+ throw new SentryError(`Failed to send email: ${response.statusText}`, {
44
+ sentryMessage: `Failed to send email to ${data.to} with subject ${data.subject}: ${response.statusText}`,
45
+ });
21
46
  }
22
- return response;
23
47
  };
24
48
  //# sourceMappingURL=send-email.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"send-email.js","sourceRoot":"src/","sources":["utils/send-email.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,SAAS,GAAG,KAAK,EAAE,EAC9B,cAAc,EACd,IAAI,GAUL,EAAE,EAAE;IACH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,mCAAmC,EAAE;QAChE,MAAM,EAAE,MAAM;QACd,OAAO,EAAE;YACP,MAAM,EAAE,kBAAkB;YAC1B,cAAc,EAAE,kBAAkB;YAClC,yBAAyB,EAAE,cAAc;SAC1C;QACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;YACnB,IAAI,EAAE,2CAA2C;YACjD,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;SAC1B,CAAC;KACH,CAAC,CAAC;IAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CAAC,yBAAyB,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAC;IAClE,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC,CAAC"}
1
+ {"version":3,"file":"send-email.js","sourceRoot":"src/","sources":["utils/send-email.ts"],"names":[],"mappings":"AAQA,MAAM,OAAO,WAAY,SAAQ,KAAK;IAKpC,YAAY,OAAe,EAAE,UAA8B,EAAE;QAC3D,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,aAAa,CAAC;QAC1B,IAAI,CAAC,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,KAAK,CAAC;QAC9C,IAAI,CAAC,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC;QAC3C,IAAI,CAAC,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;IACvC,CAAC;CACF;AAED,MAAM,CAAC,MAAM,SAAS,GAAG,KAAK,EAAE,EAC9B,cAAc,EACd,IAAI,EACJ,MAAM,GAWP,EAAE,EAAE;IACH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,mCAAmC,EAAE;QAChE,MAAM,EAAE,MAAM;QACd,OAAO,EAAE;YACP,MAAM,EAAE,kBAAkB;YAC1B,cAAc,EAAE,kBAAkB;YAClC,yBAAyB,EAAE,cAAc;SAC1C;QACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;YACnB,IAAI,EAAE,2CAA2C;YACjD,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;SAC1B,CAAC;KACH,CAAC,CAAC;IAEH,MAAM,IAAI,GAA4B,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAE9E,MAAM,CAAC,IAAI,CAAC,mBAAmB,EAAE;QAC/B,MAAM,EAAE,QAAQ,CAAC,MAAM;QACvB,UAAU,EAAE,QAAQ,CAAC,UAAU;QAC/B,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;KACpC,CAAC,CAAC;IAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,IACE,QAAQ,CAAC,MAAM,KAAK,GAAG;YACvB,SAAS,IAAI,IAAI;YACjB,OAAO,IAAI,CAAC,OAAO,KAAK,QAAQ,EAChC,CAAC;YACD,MAAM,IAAI,WAAW,CAAC,IAAI,CAAC,OAAO,EAAE;gBAClC,UAAU,EAAE,IAAI;gBAChB,UAAU,EAAE,GAAG;aAChB,CAAC,CAAC;QACL,CAAC;QAED,MAAM,IAAI,WAAW,CAAC,yBAAyB,QAAQ,CAAC,UAAU,EAAE,EAAE;YACpE,aAAa,EAAE,2BAA2B,IAAI,CAAC,EAAE,iBAAiB,IAAI,CAAC,OAAO,KAAK,QAAQ,CAAC,UAAU,EAAE;SACzG,CAAC,CAAC;IACL,CAAC;AACH,CAAC,CAAC"}
@@ -0,0 +1,11 @@
1
+ import type { EmailBaseProps } from '../types';
2
+ type NotificationKey = '$ownerStatementInReview' | '$ownerStatementPublished' | '$inviteOwnerAccepted' | '$inviteTeamMemberAccepted' | '$1099StatementPublished';
3
+ export type DailyDigestEmailTemplateProps = {
4
+ mode: EmailBaseProps['mode'];
5
+ unreadNotifications: Record<NotificationKey, {
6
+ id: string;
7
+ name: string;
8
+ }[]>;
9
+ };
10
+ export declare const DailyDigestEmailTemplate: ({ mode, unreadNotifications, }: DailyDigestEmailTemplateProps) => import("react/jsx-runtime").JSX.Element;
11
+ export {};
@@ -0,0 +1,97 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Heading } from '@react-email/heading';
3
+ import { Link } from '@react-email/link';
4
+ import { Text } from '@react-email/text';
5
+ import { EmailContainer } from '../components';
6
+ import { Button } from '../components/Button';
7
+ const THEME_VRP = [
8
+ // baseColor: #012F64
9
+ '#E6F1FF',
10
+ '#B8D9FE',
11
+ '#8BC0FE',
12
+ '#5DA8FD',
13
+ '#308FFD',
14
+ '#0377FC',
15
+ '#025FCA',
16
+ '#024797',
17
+ '#012F65',
18
+ '#011832',
19
+ ];
20
+ const THEME_NEUTRAL = [
21
+ '#fafafa',
22
+ '#f5f5f5',
23
+ '#e5e5e5',
24
+ '#d4d4d4',
25
+ '#a3a3a3',
26
+ '#737373',
27
+ '#525252',
28
+ '#404040',
29
+ '#262626',
30
+ '#171717',
31
+ // "#0a0a0a"
32
+ ];
33
+ const NOTIFICATION_ORDER = {
34
+ $inviteOwnerAccepted: {
35
+ title: 'Owners invitations',
36
+ text: 'accepted invite',
37
+ order: 1,
38
+ },
39
+ $inviteTeamMemberAccepted: {
40
+ title: 'Team member invitations',
41
+ text: 'accepted invite',
42
+ order: 2,
43
+ },
44
+ $ownerStatementInReview: {
45
+ title: 'Owner statements ready for review',
46
+ text: 'is ready for review',
47
+ order: 3,
48
+ },
49
+ $ownerStatementPublished: {
50
+ title: 'Owner statements were published',
51
+ text: 'was published to owner portal',
52
+ order: 4,
53
+ },
54
+ $1099StatementPublished: {
55
+ title: '1099 tax statements were published',
56
+ text: 'was finalized and published',
57
+ order: 5,
58
+ },
59
+ };
60
+ export const DailyDigestEmailTemplate = ({ mode, unreadNotifications, }) => {
61
+ const total = Object.values(unreadNotifications).reduce((acc, curr) => acc + curr.length, 0);
62
+ const emailTitle = `You have ${total} unread notification${total > 1 ? 's' : ''}`;
63
+ const link = 'https://portal.vrplatform.app/dashboard/inbox';
64
+ return (_jsxs(EmailContainer, { title: emailTitle, shortDescription: "Login to VRPlatform now to check your team's activity!", tenant: {
65
+ logo: undefined,
66
+ }, mode: mode, children: [_jsx(Heading, { style: {
67
+ fontSize: 26,
68
+ fontWeight: 600,
69
+ marginBottom: '50px',
70
+ }, children: emailTitle }), Object.entries(unreadNotifications)
71
+ .sort(([aKey], [bKey]) => {
72
+ const a = NOTIFICATION_ORDER[aKey]?.order ?? 0;
73
+ const b = NOTIFICATION_ORDER[bKey]?.order ?? 0;
74
+ return a - b;
75
+ })
76
+ .map(([key, notifications]) => {
77
+ const data = NOTIFICATION_ORDER[key];
78
+ if (notifications.length === 0 || !data)
79
+ return null;
80
+ return (_jsxs("div", { style: {
81
+ marginBottom: '42px',
82
+ }, children: [_jsx(Text, { style: {
83
+ color: THEME_NEUTRAL[5],
84
+ }, children: data.title }), notifications.map((not) => (_jsxs(Text, { style: { color: THEME_NEUTRAL[5] }, children: [_jsx("span", { style: {
85
+ fontWeight: 800,
86
+ color: '#000',
87
+ }, children: not.name }), ' ', data.text] }, not.id)))] }, key));
88
+ }), _jsx(Button, { href: link, mode: mode, "data-testid": "accept-invite-button", label: 'Go to VRPlatform', themeColors: THEME_VRP, style: {
89
+ marginTop: '50px',
90
+ marginBottom: '50px',
91
+ } }), _jsx(Text, { children: "Best, VRPlatform" }), _jsxs(Text, { children: ["If you have any trouble with the button, you can copy and paste the link below into your browser:", ' ', _jsx(Link, { href: link, style: {
92
+ display: 'inline',
93
+ wordBreak: 'break-all',
94
+ color: THEME_VRP[7],
95
+ }, children: link })] })] }));
96
+ };
97
+ //# sourceMappingURL=DailyDigestEmailTemplate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DailyDigestEmailTemplate.js","sourceRoot":"src/","sources":["templates/DailyDigestEmailTemplate.tsx"],"names":[],"mappings":"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"}
@@ -5,3 +5,4 @@ export * from './OwnerStatementEmailTemplate';
5
5
  export * from './OwnerTaxStatementEmailTemplate';
6
6
  export * from './InviteOwnerEmailTemplate';
7
7
  export * from './InvitePmEmailTemplate';
8
+ export * from './DailyDigestEmailTemplate';
@@ -9,4 +9,6 @@ export * from './OwnerTaxStatementEmailTemplate';
9
9
  // Invitations
10
10
  export * from './InviteOwnerEmailTemplate';
11
11
  export * from './InvitePmEmailTemplate';
12
+ // Daily Digest
13
+ export * from './DailyDigestEmailTemplate';
12
14
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"src/","sources":["templates/index.ts"],"names":[],"mappings":"AAAA,QAAQ;AACR,cAAc,iCAAiC,CAAC;AAChD,cAAc,+BAA+B,CAAC;AAE9C,mBAAmB;AACnB,cAAc,sCAAsC,CAAC;AACrD,cAAc,+BAA+B,CAAC;AAE9C,uBAAuB;AACvB,cAAc,kCAAkC,CAAC;AAEjD,cAAc;AACd,cAAc,4BAA4B,CAAC;AAC3C,cAAc,yBAAyB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"src/","sources":["templates/index.ts"],"names":[],"mappings":"AAAA,QAAQ;AACR,cAAc,iCAAiC,CAAC;AAChD,cAAc,+BAA+B,CAAC;AAE9C,mBAAmB;AACnB,cAAc,sCAAsC,CAAC;AACrD,cAAc,+BAA+B,CAAC;AAE9C,uBAAuB;AACvB,cAAc,kCAAkC,CAAC;AAEjD,cAAc;AACd,cAAc,4BAA4B,CAAC;AAC3C,cAAc,yBAAyB,CAAC;AAExC,eAAe;AACf,cAAc,4BAA4B,CAAC"}
@@ -1,4 +1,4 @@
1
- import type { Maybe } from '@finalytic/utils';
1
+ import type { Maybe } from '@vrplatform/utils';
2
2
  export declare const getOwnerPortalColor: (color: Maybe<string>) => string | undefined;
3
3
  export declare const OWNER_PORTAL_COLORS: {
4
4
  orange: string;
@@ -1 +1 @@
1
- {"root":["../../src/const.ts","../../src/extensions.ts","../../src/index.ts","../../src/theme.ts","../../src/types.ts","../../src/components/actionemailtemplate.tsx","../../src/components/button.tsx","../../src/components/emailcontainer.tsx","../../src/components/index.ts","../../src/components/utils/index.ts","../../src/components/utils/parse-email-html.test.ts","../../src/components/utils/parse-email-html.ts","../../src/templates/inviteowneremailtemplate.tsx","../../src/templates/invitepmemailtemplate.tsx","../../src/templates/ownerstatementemailtemplate.tsx","../../src/templates/ownerstatementsummaryemailtemplate.tsx","../../src/templates/ownertaxstatementemailtemplate.tsx","../../src/templates/tenantdeletionemailtemplate.tsx","../../src/templates/tenantsuspensionemailtemplate.tsx","../../src/templates/index.ts","../../src/utils/index.ts","../../src/utils/send-email.ts"],"version":"5.8.3"}
1
+ {"root":["../../src/const.ts","../../src/extensions.ts","../../src/index.ts","../../src/liveblocks.d.ts","../../src/theme.ts","../../src/types.ts","../../src/components/actionemailtemplate.tsx","../../src/components/button.tsx","../../src/components/emailcontainer.tsx","../../src/components/index.ts","../../src/components/utils/index.ts","../../src/components/utils/parse-email-html.test.ts","../../src/components/utils/parse-email-html.ts","../../src/templates/dailydigestemailtemplate.tsx","../../src/templates/inviteowneremailtemplate.tsx","../../src/templates/invitepmemailtemplate.tsx","../../src/templates/ownerstatementemailtemplate.tsx","../../src/templates/ownerstatementsummaryemailtemplate.tsx","../../src/templates/ownertaxstatementemailtemplate.tsx","../../src/templates/tenantdeletionemailtemplate.tsx","../../src/templates/tenantsuspensionemailtemplate.tsx","../../src/templates/index.ts","../../src/utils/index.ts","../../src/utils/send-email.ts"],"version":"5.9.3"}
@@ -1,5 +1,18 @@
1
- export declare const sendEmail: ({ POSTMARK_TOKEN, data, }: {
1
+ import type { logger as log } from '@trigger.dev/sdk';
2
+ type SentryErrorOptions = {
3
+ skipSentry?: boolean;
4
+ sentryMessage?: string;
5
+ statusCode?: number;
6
+ };
7
+ export declare class SentryError extends Error {
8
+ readonly skipSentry: boolean;
9
+ readonly sentryMessage?: string;
10
+ readonly statusCode?: number;
11
+ constructor(message: string, options?: SentryErrorOptions);
12
+ }
13
+ export declare const sendEmail: ({ POSTMARK_TOKEN, data, logger, }: {
2
14
  POSTMARK_TOKEN: string;
15
+ logger: typeof log;
3
16
  data: {
4
17
  to: string;
5
18
  subject: string;
@@ -7,4 +20,5 @@ export declare const sendEmail: ({ POSTMARK_TOKEN, data, }: {
7
20
  html: string;
8
21
  replyTo?: string;
9
22
  };
10
- }) => Promise<Response>;
23
+ }) => Promise<void>;
24
+ export {};
@@ -1,4 +1,16 @@
1
- export const sendEmail = async ({ POSTMARK_TOKEN, data, }) => {
1
+ export class SentryError extends Error {
2
+ skipSentry;
3
+ sentryMessage;
4
+ statusCode;
5
+ constructor(message, options = {}) {
6
+ super(message);
7
+ this.name = 'SentryError';
8
+ this.skipSentry = options.skipSentry ?? false;
9
+ this.sentryMessage = options.sentryMessage;
10
+ this.statusCode = options.statusCode;
11
+ }
12
+ }
13
+ export const sendEmail = async ({ POSTMARK_TOKEN, data, logger, }) => {
2
14
  const response = await fetch('https://api.postmarkapp.com/email', {
3
15
  method: 'POST',
4
16
  headers: {
@@ -16,9 +28,24 @@ export const sendEmail = async ({ POSTMARK_TOKEN, data, }) => {
16
28
  MessageStream: 'outbound',
17
29
  }),
18
30
  });
31
+ const json = await response.json().catch(() => ({}));
32
+ logger.info('Postmark Response', {
33
+ status: response.status,
34
+ statusText: response.statusText,
35
+ body: JSON.stringify(json, null, 2),
36
+ });
19
37
  if (!response.ok) {
20
- throw new Error(`Failed to send email: ${response.statusText}`);
38
+ if (response.status === 422 &&
39
+ 'Message' in json &&
40
+ typeof json.Message === 'string') {
41
+ throw new SentryError(json.Message, {
42
+ skipSentry: true,
43
+ statusCode: 422,
44
+ });
45
+ }
46
+ throw new SentryError(`Failed to send email: ${response.statusText}`, {
47
+ sentryMessage: `Failed to send email to ${data.to} with subject ${data.subject}: ${response.statusText}`,
48
+ });
21
49
  }
22
- return response;
23
50
  };
24
51
  //# sourceMappingURL=send-email.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"send-email.js","sourceRoot":"src/","sources":["utils/send-email.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,SAAS,GAAG,KAAK,EAAE,EAC9B,cAAc,EACd,IAAI,GAUL,EAAE,EAAE;IACH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,mCAAmC,EAAE;QAChE,MAAM,EAAE,MAAM;QACd,OAAO,EAAE;YACP,MAAM,EAAE,kBAAkB;YAC1B,cAAc,EAAE,kBAAkB;YAClC,yBAAyB,EAAE,cAAc;SAC1C;QACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;YACnB,IAAI,EAAE,2CAA2C;YACjD,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;SAC1B,CAAC;KACH,CAAC,CAAC;IAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CAAC,yBAAyB,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAC;IAClE,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC,CAAC"}
1
+ {"version":3,"file":"send-email.js","sourceRoot":"src/","sources":["utils/send-email.ts"],"names":[],"mappings":"AAQA,MAAM,OAAO,WAAY,SAAQ,KAAK;IAC3B,UAAU,CAAU;IACpB,aAAa,CAAU;IACvB,UAAU,CAAU;IAE7B,YAAY,OAAe,EAAE,UAA8B,EAAE;QAC3D,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,aAAa,CAAC;QAC1B,IAAI,CAAC,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,KAAK,CAAC;QAC9C,IAAI,CAAC,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC;QAC3C,IAAI,CAAC,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;IACvC,CAAC;CACF;AAED,MAAM,CAAC,MAAM,SAAS,GAAG,KAAK,EAAE,EAC9B,cAAc,EACd,IAAI,EACJ,MAAM,GAWP,EAAE,EAAE;IACH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,mCAAmC,EAAE;QAChE,MAAM,EAAE,MAAM;QACd,OAAO,EAAE;YACP,MAAM,EAAE,kBAAkB;YAC1B,cAAc,EAAE,kBAAkB;YAClC,yBAAyB,EAAE,cAAc;SAC1C;QACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;YACnB,IAAI,EAAE,2CAA2C;YACjD,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;SAC1B,CAAC;KACH,CAAC,CAAC;IAEH,MAAM,IAAI,GAA4B,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAE9E,MAAM,CAAC,IAAI,CAAC,mBAAmB,EAAE;QAC/B,MAAM,EAAE,QAAQ,CAAC,MAAM;QACvB,UAAU,EAAE,QAAQ,CAAC,UAAU;QAC/B,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;KACpC,CAAC,CAAC;IAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,IACE,QAAQ,CAAC,MAAM,KAAK,GAAG;YACvB,SAAS,IAAI,IAAI;YACjB,OAAO,IAAI,CAAC,OAAO,KAAK,QAAQ,EAChC,CAAC;YACD,MAAM,IAAI,WAAW,CAAC,IAAI,CAAC,OAAO,EAAE;gBAClC,UAAU,EAAE,IAAI;gBAChB,UAAU,EAAE,GAAG;aAChB,CAAC,CAAC;QACL,CAAC;QAED,MAAM,IAAI,WAAW,CAAC,yBAAyB,QAAQ,CAAC,UAAU,EAAE,EAAE;YACpE,aAAa,EAAE,2BAA2B,IAAI,CAAC,EAAE,iBAAiB,IAAI,CAAC,OAAO,KAAK,QAAQ,CAAC,UAAU,EAAE;SACzG,CAAC,CAAC;IACL,CAAC;AACH,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vrplatform/email",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "keywords": [],
5
5
  "license": "ISC",
6
6
  "main": "build/main/index.js",
@@ -16,7 +16,7 @@
16
16
  "access": "public"
17
17
  },
18
18
  "dependencies": {
19
- "@finalytic/utils": "*",
19
+ "@vrplatform/utils": "*",
20
20
  "@react-email/html": "0.0.9",
21
21
  "@react-email/body": "0.0.9",
22
22
  "@react-email/button": "0.0.16",
@@ -28,17 +28,17 @@
28
28
  "@react-email/img": "0.0.9",
29
29
  "@react-email/render": "0.0.17",
30
30
  "@react-email/preview": "0.0.10",
31
- "@react-email/link": "0.0.9"
31
+ "@react-email/link": "0.0.9",
32
+ "@trigger.dev/sdk": "*"
32
33
  },
33
34
  "devDependencies": {
34
- "@biomejs/biome": "*",
35
35
  "@types/bun": "*",
36
- "@types/react": "^19.0.12",
36
+ "@types/react": "19.2.7",
37
37
  "react-email": "3.0.1",
38
38
  "typescript": "*"
39
39
  },
40
40
  "scripts": {
41
- "pub": "bun run build && pnpm publish --no-git-checks",
41
+ "pub": "bun run build && pnpm version patch --no-commit-hooks --no-git-tag-version --no-workspaces-update && pnpm publish --no-git-checks",
42
42
  "build": "bun build:main & bun build:module",
43
43
  "build:main": "tsc -b tsconfig.main.json",
44
44
  "build:module": "tsc -b tsconfig.esm.json",
@@ -0,0 +1,88 @@
1
+ // Define Liveblocks types for your application
2
+ // https://liveblocks.io/docs/api-reference/liveblocks-react#Typing-your-data
3
+ declare global {
4
+ interface Liveblocks {
5
+ ActivitiesData: {
6
+ $ownerStatementInReview: {
7
+ ownerStatementId: string;
8
+ listingId: string;
9
+ startAt: string;
10
+ tenantId: string;
11
+ triggeredByUserId: string;
12
+ };
13
+ $ownerStatementPublished: {
14
+ ownerStatementId: string;
15
+ tenantId: string;
16
+ listingId: string;
17
+ startAt: string;
18
+ status: 'posted' | 'published';
19
+ triggeredByUserId?: string;
20
+ };
21
+ $inviteOwnerAccepted: {
22
+ userId: string;
23
+ tenantId: string;
24
+ };
25
+ $inviteTeamMemberAccepted: {
26
+ userId: string;
27
+ tenantId: string;
28
+ };
29
+ $1099StatementPublished: {
30
+ taxStatementId: string;
31
+ tenantId: string;
32
+ year: number;
33
+ status: 'ready' | 'submitted';
34
+ triggeredByUserId?: string;
35
+ };
36
+ };
37
+
38
+ // Each user's Presence, for useMyPresence, useOthers, etc.
39
+ // biome-ignore lint/complexity/noBannedTypes: <explanation>
40
+ Presence: {
41
+ // Example, real-time cursor coordinates
42
+ // cursor: { x: number; y: number };
43
+ };
44
+
45
+ // The Storage tree for the room, for useMutation, useStorage, etc.
46
+ // biome-ignore lint/complexity/noBannedTypes: <explanation>
47
+ Storage: {
48
+ // Example, a conflict-free list
49
+ // animals: LiveList<string>;
50
+ };
51
+
52
+ // Custom user info set when authenticating with a secret key
53
+ UserMeta: {
54
+ id: string;
55
+ // biome-ignore lint/complexity/noBannedTypes: <explanation>
56
+ info: {
57
+ // Example properties, for useSelf, useUser, useOthers, etc.
58
+ // name: string;
59
+ // avatar: string;
60
+ };
61
+ };
62
+
63
+ // Custom events, for useBroadcastEvent, useEventListener
64
+ // biome-ignore lint/complexity/noBannedTypes: <explanation>
65
+ RoomEvent: {};
66
+ // Example has two events, using a union
67
+ // | { type: "PLAY" }
68
+ // | { type: "REACTION"; emoji: "🔥" };
69
+
70
+ // Custom metadata set on threads, for useThreads, useCreateThread, etc.
71
+ // biome-ignore lint/complexity/noBannedTypes: <explanation>
72
+ ThreadMetadata: {
73
+ // Example, attaching coordinates to a thread
74
+ // x: number;
75
+ // y: number;
76
+ };
77
+
78
+ // Custom room info set with resolveRoomsInfo, for useRoomInfo
79
+ // biome-ignore lint/complexity/noBannedTypes: <explanation>
80
+ RoomInfo: {
81
+ // Example, rooms with a title and url
82
+ // title: string;
83
+ // url: string;
84
+ };
85
+ }
86
+ }
87
+
88
+ export type {};
@@ -0,0 +1,192 @@
1
+ import { Heading } from '@react-email/heading';
2
+ import { Link } from '@react-email/link';
3
+ import { Text } from '@react-email/text';
4
+ import { EmailContainer } from '../components';
5
+ import { Button } from '../components/Button';
6
+ import type { EmailBaseProps } from '../types';
7
+
8
+ // type NotificationKey = keyof Liveblocks['ActivitiesData'];
9
+ type NotificationKey =
10
+ | '$ownerStatementInReview'
11
+ | '$ownerStatementPublished'
12
+ | '$inviteOwnerAccepted'
13
+ | '$inviteTeamMemberAccepted'
14
+ | '$1099StatementPublished';
15
+
16
+ export type DailyDigestEmailTemplateProps = {
17
+ mode: EmailBaseProps['mode'];
18
+ unreadNotifications: Record<NotificationKey, { id: string; name: string }[]>;
19
+ };
20
+
21
+ const THEME_VRP = [
22
+ // baseColor: #012F64
23
+ '#E6F1FF',
24
+ '#B8D9FE',
25
+ '#8BC0FE',
26
+ '#5DA8FD',
27
+ '#308FFD',
28
+ '#0377FC',
29
+ '#025FCA',
30
+ '#024797',
31
+ '#012F65',
32
+ '#011832',
33
+ ] as const;
34
+
35
+ const THEME_NEUTRAL = [
36
+ '#fafafa',
37
+ '#f5f5f5',
38
+ '#e5e5e5',
39
+ '#d4d4d4',
40
+ '#a3a3a3',
41
+ '#737373',
42
+ '#525252',
43
+ '#404040',
44
+ '#262626',
45
+ '#171717',
46
+ // "#0a0a0a"
47
+ ];
48
+
49
+ const NOTIFICATION_ORDER: Record<
50
+ NotificationKey,
51
+ {
52
+ title: string;
53
+ text: string;
54
+ order: number;
55
+ }
56
+ > = {
57
+ $inviteOwnerAccepted: {
58
+ title: 'Owners invitations',
59
+ text: 'accepted invite',
60
+ order: 1,
61
+ },
62
+ $inviteTeamMemberAccepted: {
63
+ title: 'Team member invitations',
64
+ text: 'accepted invite',
65
+ order: 2,
66
+ },
67
+ $ownerStatementInReview: {
68
+ title: 'Owner statements ready for review',
69
+ text: 'is ready for review',
70
+ order: 3,
71
+ },
72
+ $ownerStatementPublished: {
73
+ title: 'Owner statements were published',
74
+ text: 'was published to owner portal',
75
+ order: 4,
76
+ },
77
+ $1099StatementPublished: {
78
+ title: '1099 tax statements were published',
79
+ text: 'was finalized and published',
80
+ order: 5,
81
+ },
82
+ };
83
+
84
+ export const DailyDigestEmailTemplate = ({
85
+ mode,
86
+
87
+ unreadNotifications,
88
+ }: DailyDigestEmailTemplateProps) => {
89
+ const total = Object.values(unreadNotifications).reduce(
90
+ (acc, curr) => acc + curr.length,
91
+ 0
92
+ );
93
+
94
+ const emailTitle = `You have ${total} unread notification${total > 1 ? 's' : ''}`;
95
+
96
+ const link = 'https://portal.vrplatform.app/dashboard/inbox';
97
+
98
+ return (
99
+ <EmailContainer
100
+ title={emailTitle}
101
+ shortDescription={
102
+ "Login to VRPlatform now to check your team's activity!"
103
+ }
104
+ tenant={{
105
+ logo: undefined,
106
+ }}
107
+ mode={mode}
108
+ >
109
+ <Heading
110
+ style={{
111
+ fontSize: 26,
112
+ fontWeight: 600,
113
+ marginBottom: '50px',
114
+ }}
115
+ >
116
+ {emailTitle}
117
+ </Heading>
118
+
119
+ {Object.entries(unreadNotifications)
120
+ .sort(([aKey], [bKey]) => {
121
+ const a = NOTIFICATION_ORDER[aKey as NotificationKey]?.order ?? 0;
122
+ const b = NOTIFICATION_ORDER[bKey as NotificationKey]?.order ?? 0;
123
+
124
+ return a - b;
125
+ })
126
+ .map(([key, notifications]) => {
127
+ const data = NOTIFICATION_ORDER[key as NotificationKey];
128
+
129
+ if (notifications.length === 0 || !data) return null;
130
+
131
+ return (
132
+ <div
133
+ key={key}
134
+ style={{
135
+ marginBottom: '42px',
136
+ }}
137
+ >
138
+ <Text
139
+ style={{
140
+ color: THEME_NEUTRAL[5],
141
+ }}
142
+ >
143
+ {data.title}
144
+ </Text>
145
+ {notifications.map((not) => (
146
+ <Text key={not.id} style={{ color: THEME_NEUTRAL[5] }}>
147
+ <span
148
+ style={{
149
+ fontWeight: 800,
150
+ color: '#000',
151
+ }}
152
+ >
153
+ {not.name}
154
+ </span>{' '}
155
+ {data.text}
156
+ </Text>
157
+ ))}
158
+ </div>
159
+ );
160
+ })}
161
+
162
+ <Button
163
+ href={link}
164
+ mode={mode}
165
+ data-testid="accept-invite-button"
166
+ label={'Go to VRPlatform'}
167
+ themeColors={THEME_VRP}
168
+ style={{
169
+ marginTop: '50px',
170
+ marginBottom: '50px',
171
+ }}
172
+ />
173
+
174
+ <Text>Best, VRPlatform</Text>
175
+
176
+ <Text>
177
+ If you have any trouble with the button, you can copy and paste the link
178
+ below into your browser:{' '}
179
+ <Link
180
+ href={link}
181
+ style={{
182
+ display: 'inline',
183
+ wordBreak: 'break-all',
184
+ color: THEME_VRP[7],
185
+ }}
186
+ >
187
+ {link}
188
+ </Link>
189
+ </Text>
190
+ </EmailContainer>
191
+ );
192
+ };
@@ -12,3 +12,6 @@ export * from './OwnerTaxStatementEmailTemplate';
12
12
  // Invitations
13
13
  export * from './InviteOwnerEmailTemplate';
14
14
  export * from './InvitePmEmailTemplate';
15
+
16
+ // Daily Digest
17
+ export * from './DailyDigestEmailTemplate';
package/src/theme.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Maybe } from '@finalytic/utils';
1
+ import type { Maybe } from '@vrplatform/utils';
2
2
 
3
3
  export const getOwnerPortalColor = (color: Maybe<string>) => {
4
4
  if (!color) return undefined;
@@ -1,8 +1,32 @@
1
+ import type { logger as log } from '@trigger.dev/sdk';
2
+
3
+ type SentryErrorOptions = {
4
+ skipSentry?: boolean;
5
+ sentryMessage?: string;
6
+ statusCode?: number;
7
+ };
8
+
9
+ export class SentryError extends Error {
10
+ readonly skipSentry: boolean;
11
+ readonly sentryMessage?: string;
12
+ readonly statusCode?: number;
13
+
14
+ constructor(message: string, options: SentryErrorOptions = {}) {
15
+ super(message);
16
+ this.name = 'SentryError';
17
+ this.skipSentry = options.skipSentry ?? false;
18
+ this.sentryMessage = options.sentryMessage;
19
+ this.statusCode = options.statusCode;
20
+ }
21
+ }
22
+
1
23
  export const sendEmail = async ({
2
24
  POSTMARK_TOKEN,
3
25
  data,
26
+ logger,
4
27
  }: {
5
28
  POSTMARK_TOKEN: string;
29
+ logger: typeof log;
6
30
  data: {
7
31
  to: string;
8
32
  subject: string;
@@ -29,9 +53,28 @@ export const sendEmail = async ({
29
53
  }),
30
54
  });
31
55
 
56
+ const json: Record<string, unknown> = await response.json().catch(() => ({}));
57
+
58
+ logger.info('Postmark Response', {
59
+ status: response.status,
60
+ statusText: response.statusText,
61
+ body: JSON.stringify(json, null, 2),
62
+ });
63
+
32
64
  if (!response.ok) {
33
- throw new Error(`Failed to send email: ${response.statusText}`);
34
- }
65
+ if (
66
+ response.status === 422 &&
67
+ 'Message' in json &&
68
+ typeof json.Message === 'string'
69
+ ) {
70
+ throw new SentryError(json.Message, {
71
+ skipSentry: true,
72
+ statusCode: 422,
73
+ });
74
+ }
35
75
 
36
- return response;
76
+ throw new SentryError(`Failed to send email: ${response.statusText}`, {
77
+ sentryMessage: `Failed to send email to ${data.to} with subject ${data.subject}: ${response.statusText}`,
78
+ });
79
+ }
37
80
  };