@allbluecn/web-app 0.11.0 → 0.11.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.
- package/package.json +6 -3
- package/public/favicon-dark.svg +1 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.svg +1 -0
- package/public/logo/allblue-logo-dark.png +0 -0
- package/public/logo/allblue-logo-light.png +0 -0
- package/src/__tests__/integration/login-email-acceptance.test.ts +202 -0
- package/src/__tests__/integration/login-email-p2-acceptance.test.ts +142 -0
- package/src/components/auth/icon-input.tsx +9 -1
- package/src/components/auth/password-input.tsx +9 -1
- package/src/components/changelog/changelog-page.tsx +44 -1
- package/src/components/forms/app-field.tsx +3 -1
- package/src/components/knowledge/knowledge-toolbar.tsx +17 -21
- package/src/components/layout/sidebar-03/app-sidebar.tsx +4 -15
- package/src/components/notifications/settings-notifications.tsx +41 -1
- package/src/components/settings/personal-team/account/settings-profile.tsx +68 -1
- package/src/lib/__tests__/edition-desktop.test.ts +23 -0
- package/src/lib/app-version.ts +1 -1
- package/src/lib/auth/auth.config.ts +123 -99
- package/src/lib/changelog/sdk-versions.ts +4 -4
- package/src/lib/edition-desktop.ts +22 -0
- package/src/lib/session.ts +2 -2
- package/src/routeTree.gen.ts +114 -0
- package/src/routes/__root.tsx +20 -3
- package/src/routes/__tests__/-auth-schemas.test.ts +62 -0
- package/src/routes/__tests__/-edition-routing.test.tsx +14 -1
- package/src/routes/__tests__/route-tree-structure.test.ts +1 -0
- package/src/routes/_login/changelog/confirm/route.lazy.tsx +72 -0
- package/src/routes/_login/changelog/confirm/route.tsx +28 -0
- package/src/routes/_login/email-unsubscribe/route.lazy.tsx +76 -0
- package/src/routes/_login/email-unsubscribe/route.tsx +28 -0
- package/src/routes/_login/forgot-password/route.lazy.tsx +2 -1
- package/src/routes/_login/login/route.lazy.tsx +4 -2
- package/src/routes/_login/register/route.lazy.tsx +28 -17
- package/src/routes/_login/register/route.tsx +8 -1
- package/src/routes/_login/reset-password/route.lazy.tsx +4 -2
- package/src/routes/_login/schemas/-auth.ts +30 -7
- package/src/routes/_login/verify-email/route.lazy.tsx +82 -0
- package/src/routes/_login/verify-email/route.tsx +28 -0
- package/src/routes/_nav/route.tsx +5 -0
- package/src/routes/_public/index/route.tsx +2 -1
- package/src/routes/api/auth/desktop-callback.ts +2 -2
- package/src/routes/api/billing-webhook.ts +4 -0
- package/src/routes/api/cron/changelog-release.ts +100 -0
- package/src/routes/api/cron/subscription-reminders.ts +110 -0
- package/src/routes/api.auth.$.ts +3 -5
- package/src/server/__tests__/auth-registration.test.ts +60 -0
- package/src/server/auth-registration.ts +40 -0
- package/src/server/billing-email.ts +39 -0
- package/src/server/email/__tests__/deliverability.test.ts +68 -0
- package/src/server/email/__tests__/index.test.ts +32 -0
- package/src/server/email/__tests__/outbox.test.tsx +129 -0
- package/src/server/email/__tests__/product-templates.test.tsx +106 -0
- package/src/server/email/__tests__/registry.test.tsx +50 -0
- package/src/server/email/deliverability.ts +57 -0
- package/src/server/email/drivers/console.ts +11 -0
- package/src/server/email/drivers/resend.ts +31 -0
- package/src/server/email/drivers/smtp.ts +27 -0
- package/src/server/email/index.ts +56 -0
- package/src/server/email/outbox.ts +141 -0
- package/src/server/email/preferences.ts +20 -0
- package/src/server/email/registry.ts +38 -0
- package/src/server/email/sender.ts +34 -17
- package/src/server/email/templates/_base/button.tsx +12 -0
- package/src/server/email/templates/_base/email-layout.tsx +117 -0
- package/src/server/email/templates/_base/theme-fonts.tsx +43 -0
- package/src/server/email/templates/_base/theme.ts +22 -0
- package/src/server/email/templates/auth/reset-password.tsx +58 -0
- package/src/server/email/templates/auth/verify-email.tsx +59 -0
- package/src/server/email/templates/auth/welcome.tsx +49 -0
- package/src/server/email/templates/billing/payment-receipt.tsx +51 -0
- package/src/server/email/templates/billing/subscription-canceled.tsx +51 -0
- package/src/server/email/templates/billing/subscription-ending.tsx +51 -0
- package/src/server/email/templates/notification/mirror.tsx +50 -0
- package/src/server/email/templates/product/changelog-confirm.tsx +53 -0
- package/src/server/email/templates/product/changelog-release.tsx +65 -0
- package/src/server/email/templates/team/apply-notify.tsx +53 -0
- package/src/server/email/templates/team/approved.tsx +53 -0
- package/src/server/email/templates/team/invite.tsx +57 -0
- package/src/server/middlewares/security-headers.ts +11 -2
- package/src/server/notifications/__tests__/email-mirror.test.ts +193 -0
- package/src/server/notifications/__tests__/verify-reminder.test.ts +94 -0
- package/src/server/notifications/notification-service.ts +71 -1
- package/src/server/notifications/verify-reminder.ts +55 -0
- package/src/server/oauth-config.ts +59 -0
- package/src/server/serverFns/__tests__/auth-server-schemas.test.ts +168 -0
- package/src/server/serverFns/__tests__/email-verification.test.ts +149 -0
- package/src/server/serverFns/__tests__/team-invitation.test.ts +2 -2
- package/src/server/serverFns/auth/email-verification.ts +132 -0
- package/src/server/serverFns/auth/forgot-password.ts +22 -2
- package/src/server/serverFns/auth/providers.ts +4 -3
- package/src/server/serverFns/auth/register.ts +63 -19
- package/src/server/serverFns/auth/registration-mode.ts +23 -0
- package/src/server/serverFns/auth/reset-password.ts +1 -1
- package/src/server/serverFns/auth/{invitation.ts → verify-reminder.ts} +9 -26
- package/src/server/serverFns/changelog/__tests__/subscribe.test.ts +119 -0
- package/src/server/serverFns/changelog/subscribe.ts +89 -0
- package/src/server/serverFns/email-unsubscribe.ts +66 -0
- package/src/server/serverFns/notifications.ts +10 -0
- package/src/server/serverFns/team.ts +49 -11
- package/vite.shared.ts +87 -3
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Section, Text } from '@react-email/components'
|
|
2
|
+
import { EmailLayout } from '../_base/email-layout'
|
|
3
|
+
import { EmailButton } from '../_base/button'
|
|
4
|
+
import { registerTemplate, type EmailLocale } from '../../registry'
|
|
5
|
+
|
|
6
|
+
interface ResetPasswordProps {
|
|
7
|
+
locale: EmailLocale
|
|
8
|
+
url: string
|
|
9
|
+
unsubscribeUrl?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const T = {
|
|
13
|
+
zh: {
|
|
14
|
+
subject: '重置你的密码',
|
|
15
|
+
preview: '重置你的 AllBlue 密码',
|
|
16
|
+
heading: '重置密码',
|
|
17
|
+
greeting: '你好:',
|
|
18
|
+
body: '我们收到了重置你 AllBlue 账户密码的请求。请点击下方按钮设置新密码(链接 15 分钟内有效,且只能使用一次)。如果按钮无法点击,请复制以下链接到浏览器打开:',
|
|
19
|
+
cta: '重置密码',
|
|
20
|
+
ignore: '如果这不是你发起的请求,你的密码仍然安全,无需任何操作。',
|
|
21
|
+
urlLabel: '链接:',
|
|
22
|
+
},
|
|
23
|
+
en: {
|
|
24
|
+
subject: 'Reset your password',
|
|
25
|
+
preview: 'Reset your AllBlue password',
|
|
26
|
+
heading: 'Reset password',
|
|
27
|
+
greeting: 'Hello,',
|
|
28
|
+
body: 'We received a request to reset your AllBlue password. Click the button below to set a new one (the link is valid for 15 minutes and can be used only once). If the button does not work, copy this link into your browser:',
|
|
29
|
+
cta: 'Reset password',
|
|
30
|
+
ignore: 'If you did not request this, your password is safe — no action is needed.',
|
|
31
|
+
urlLabel: 'Link:',
|
|
32
|
+
},
|
|
33
|
+
} as const
|
|
34
|
+
|
|
35
|
+
function ResetPasswordEmail({ locale, url, unsubscribeUrl }: ResetPasswordProps) {
|
|
36
|
+
const t = T[locale]
|
|
37
|
+
return (
|
|
38
|
+
<EmailLayout locale={locale} preview={t.preview} heading={t.heading} unsubscribeUrl={unsubscribeUrl} footerKind="transactional">
|
|
39
|
+
<Text className="text-base text-fg-2 mt-0 mb-6 font-sans">{t.greeting}</Text>
|
|
40
|
+
<Text className="text-base text-fg-2 mx-auto mt-0 mb-8 max-w-[380px] font-sans">{t.body}</Text>
|
|
41
|
+
<Section className="mb-6 text-center">
|
|
42
|
+
<EmailButton href={url} label={t.cta} />
|
|
43
|
+
</Section>
|
|
44
|
+
<Text className="text-sm text-fg-3 break-all font-sans">{t.urlLabel} {url}</Text>
|
|
45
|
+
<Text className="text-sm text-fg-3 mt-8 font-sans">{t.ignore}</Text>
|
|
46
|
+
</EmailLayout>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function registerResetPasswordTemplate() {
|
|
51
|
+
registerTemplate({
|
|
52
|
+
key: 'auth.reset-password',
|
|
53
|
+
category: 'auth',
|
|
54
|
+
transactional: true,
|
|
55
|
+
subject: { zh: T.zh.subject, en: T.en.subject },
|
|
56
|
+
component: ResetPasswordEmail,
|
|
57
|
+
})
|
|
58
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Link, Section, Text } from '@react-email/components'
|
|
2
|
+
import { EmailLayout } from '../_base/email-layout'
|
|
3
|
+
import { EmailButton } from '../_base/button'
|
|
4
|
+
import { registerTemplate, type EmailLocale } from '../../registry'
|
|
5
|
+
|
|
6
|
+
interface VerifyEmailProps {
|
|
7
|
+
locale: EmailLocale
|
|
8
|
+
url: string
|
|
9
|
+
username: string
|
|
10
|
+
unsubscribeUrl?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const T = {
|
|
14
|
+
zh: {
|
|
15
|
+
subject: '验证您的邮箱',
|
|
16
|
+
preview: '验证您的 AllBlue 邮箱地址',
|
|
17
|
+
heading: '我们快完成了!',
|
|
18
|
+
greeting: (u: string) => `你好,${u}:`,
|
|
19
|
+
body: '感谢注册 AllBlue。请点击下方按钮验证您的邮箱地址(24 小时内有效)。如果按钮无法点击,请复制以下链接到浏览器打开:',
|
|
20
|
+
cta: '验证邮箱',
|
|
21
|
+
ignore: '如果这不是你的操作,请忽略本邮件。',
|
|
22
|
+
urlLabel: '链接:',
|
|
23
|
+
},
|
|
24
|
+
en: {
|
|
25
|
+
subject: 'Verify your email',
|
|
26
|
+
preview: 'Verify your AllBlue email address',
|
|
27
|
+
heading: "We're almost there!",
|
|
28
|
+
greeting: (u: string) => `Hi ${u},`,
|
|
29
|
+
body: 'Thank you for signing up for AllBlue. Click the button below to verify your email address (link valid for 24 hours). If the button does not work, copy this link into your browser:',
|
|
30
|
+
cta: 'Verify email',
|
|
31
|
+
ignore: 'If this was not you, please ignore this email.',
|
|
32
|
+
urlLabel: 'Link:',
|
|
33
|
+
},
|
|
34
|
+
} as const
|
|
35
|
+
|
|
36
|
+
function VerifyEmailEmail({ locale, url, username, unsubscribeUrl }: VerifyEmailProps) {
|
|
37
|
+
const t = T[locale]
|
|
38
|
+
return (
|
|
39
|
+
<EmailLayout locale={locale} preview={t.preview} heading={t.heading} unsubscribeUrl={unsubscribeUrl} footerKind="transactional">
|
|
40
|
+
<Text className="text-base text-fg-2 mt-0 mb-6 font-sans">{t.greeting(username)}</Text>
|
|
41
|
+
<Text className="text-base text-fg-2 mx-auto mt-0 mb-8 max-w-[380px] font-sans">{t.body}</Text>
|
|
42
|
+
<Section className="mb-6 text-center">
|
|
43
|
+
<EmailButton href={url} label={t.cta} />
|
|
44
|
+
</Section>
|
|
45
|
+
<Text className="text-sm text-fg-3 break-all font-sans">{t.urlLabel} {url}</Text>
|
|
46
|
+
<Text className="text-sm text-fg-3 mt-8 font-sans">{t.ignore}</Text>
|
|
47
|
+
</EmailLayout>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function registerVerifyEmailTemplate() {
|
|
52
|
+
registerTemplate({
|
|
53
|
+
key: 'auth.verify-email',
|
|
54
|
+
category: 'auth',
|
|
55
|
+
transactional: true,
|
|
56
|
+
subject: { zh: T.zh.subject, en: T.en.subject },
|
|
57
|
+
component: VerifyEmailEmail,
|
|
58
|
+
})
|
|
59
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Section, Text } from '@react-email/components'
|
|
2
|
+
import { EmailLayout } from '../_base/email-layout'
|
|
3
|
+
import { EmailButton } from '../_base/button'
|
|
4
|
+
import { registerTemplate, type EmailLocale } from '../../registry'
|
|
5
|
+
|
|
6
|
+
interface WelcomeProps {
|
|
7
|
+
locale: EmailLocale
|
|
8
|
+
dashboardUrl: string
|
|
9
|
+
unsubscribeUrl?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const T = {
|
|
13
|
+
zh: {
|
|
14
|
+
subject: '邮箱验证成功,欢迎加入 AllBlue',
|
|
15
|
+
preview: '欢迎加入 AllBlue',
|
|
16
|
+
heading: '欢迎加入 AllBlue',
|
|
17
|
+
body: '你的邮箱已验证成功。现在可以完整使用 AllBlue 的全部功能了。',
|
|
18
|
+
cta: '进入工作台',
|
|
19
|
+
},
|
|
20
|
+
en: {
|
|
21
|
+
subject: 'Email verified — welcome to AllBlue',
|
|
22
|
+
preview: 'Welcome to AllBlue',
|
|
23
|
+
heading: 'Welcome to AllBlue',
|
|
24
|
+
body: 'Your email has been verified. You now have full access to AllBlue.',
|
|
25
|
+
cta: 'Go to dashboard',
|
|
26
|
+
},
|
|
27
|
+
} as const
|
|
28
|
+
|
|
29
|
+
function WelcomeEmail({ locale, dashboardUrl, unsubscribeUrl }: WelcomeProps) {
|
|
30
|
+
const t = T[locale]
|
|
31
|
+
return (
|
|
32
|
+
<EmailLayout locale={locale} preview={t.preview} heading={t.heading} unsubscribeUrl={unsubscribeUrl} footerKind="transactional">
|
|
33
|
+
<Text className="text-base text-fg-2 mx-auto mt-0 mb-8 max-w-[380px] font-sans">{t.body}</Text>
|
|
34
|
+
<Section className="mb-6 text-center">
|
|
35
|
+
<EmailButton href={dashboardUrl} label={t.cta} />
|
|
36
|
+
</Section>
|
|
37
|
+
</EmailLayout>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function registerWelcomeTemplate() {
|
|
42
|
+
registerTemplate({
|
|
43
|
+
key: 'auth.welcome',
|
|
44
|
+
category: 'auth',
|
|
45
|
+
transactional: true,
|
|
46
|
+
subject: { zh: T.zh.subject, en: T.en.subject },
|
|
47
|
+
component: WelcomeEmail,
|
|
48
|
+
})
|
|
49
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Section, Text } from '@react-email/components'
|
|
2
|
+
import { EmailLayout } from '../_base/email-layout'
|
|
3
|
+
import { EmailButton } from '../_base/button'
|
|
4
|
+
import { registerTemplate, type EmailLocale } from '../../registry'
|
|
5
|
+
|
|
6
|
+
interface PaymentReceiptProps {
|
|
7
|
+
locale: EmailLocale
|
|
8
|
+
planName: string
|
|
9
|
+
periodEnd: string
|
|
10
|
+
dashboardUrl: string
|
|
11
|
+
unsubscribeUrl?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const T = {
|
|
15
|
+
zh: {
|
|
16
|
+
subject: '支付成功——AllBlue 订阅',
|
|
17
|
+
preview: '你的支付已成功',
|
|
18
|
+
heading: '支付成功',
|
|
19
|
+
body: (plan: string, end: string) => `你的 AllBlue「${plan}」订阅支付成功,当前周期至 ${end}。`,
|
|
20
|
+
cta: '查看订阅',
|
|
21
|
+
},
|
|
22
|
+
en: {
|
|
23
|
+
subject: 'Payment received — AllBlue subscription',
|
|
24
|
+
preview: 'Your payment was successful',
|
|
25
|
+
heading: 'Payment received',
|
|
26
|
+
body: (plan: string, end: string) => `Your AllBlue "${plan}" subscription payment was successful. Current period ends ${end}.`,
|
|
27
|
+
cta: 'View subscription',
|
|
28
|
+
},
|
|
29
|
+
} as const
|
|
30
|
+
|
|
31
|
+
function PaymentReceiptEmail({ locale, planName, periodEnd, dashboardUrl, unsubscribeUrl }: PaymentReceiptProps) {
|
|
32
|
+
const t = T[locale]
|
|
33
|
+
return (
|
|
34
|
+
<EmailLayout locale={locale} preview={t.preview} heading={t.heading} unsubscribeUrl={unsubscribeUrl} footerKind="transactional">
|
|
35
|
+
<Text className="text-base text-fg-2 mx-auto mt-0 mb-8 max-w-[380px] font-sans">{t.body(planName, periodEnd)}</Text>
|
|
36
|
+
<Section className="mb-6 text-center">
|
|
37
|
+
<EmailButton href={dashboardUrl} label={t.cta} />
|
|
38
|
+
</Section>
|
|
39
|
+
</EmailLayout>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function registerPaymentReceiptTemplate() {
|
|
44
|
+
registerTemplate({
|
|
45
|
+
key: 'billing.payment-receipt',
|
|
46
|
+
category: 'billing',
|
|
47
|
+
transactional: true,
|
|
48
|
+
subject: { zh: T.zh.subject, en: T.en.subject },
|
|
49
|
+
component: PaymentReceiptEmail,
|
|
50
|
+
})
|
|
51
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Section, Text } from '@react-email/components'
|
|
2
|
+
import { EmailLayout } from '../_base/email-layout'
|
|
3
|
+
import { EmailButton } from '../_base/button'
|
|
4
|
+
import { registerTemplate, type EmailLocale } from '../../registry'
|
|
5
|
+
|
|
6
|
+
interface SubscriptionCanceledProps {
|
|
7
|
+
locale: EmailLocale
|
|
8
|
+
planName: string
|
|
9
|
+
endDate: string
|
|
10
|
+
dashboardUrl: string
|
|
11
|
+
unsubscribeUrl?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const T = {
|
|
15
|
+
zh: {
|
|
16
|
+
subject: '订阅已取消',
|
|
17
|
+
preview: '你的订阅已取消',
|
|
18
|
+
heading: '订阅已取消',
|
|
19
|
+
body: (plan: string, end: string) => `你的 AllBlue「${plan}」订阅已取消,服务将持续至 ${end}。可随时重新订阅。`,
|
|
20
|
+
cta: '重新订阅',
|
|
21
|
+
},
|
|
22
|
+
en: {
|
|
23
|
+
subject: 'Subscription canceled',
|
|
24
|
+
preview: 'Your subscription was canceled',
|
|
25
|
+
heading: 'Canceled',
|
|
26
|
+
body: (plan: string, end: string) => `Your AllBlue "${plan}" subscription was canceled. Service continues until ${end}. You can resubscribe any time.`,
|
|
27
|
+
cta: 'Resubscribe',
|
|
28
|
+
},
|
|
29
|
+
} as const
|
|
30
|
+
|
|
31
|
+
function SubscriptionCanceledEmail({ locale, planName, endDate, dashboardUrl, unsubscribeUrl }: SubscriptionCanceledProps) {
|
|
32
|
+
const t = T[locale]
|
|
33
|
+
return (
|
|
34
|
+
<EmailLayout locale={locale} preview={t.preview} heading={t.heading} unsubscribeUrl={unsubscribeUrl} footerKind="transactional">
|
|
35
|
+
<Text className="text-base text-fg-2 mx-auto mt-0 mb-8 max-w-[380px] font-sans">{t.body(planName, endDate)}</Text>
|
|
36
|
+
<Section className="mb-6 text-center">
|
|
37
|
+
<EmailButton href={dashboardUrl} label={t.cta} />
|
|
38
|
+
</Section>
|
|
39
|
+
</EmailLayout>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function registerSubscriptionCanceledTemplate() {
|
|
44
|
+
registerTemplate({
|
|
45
|
+
key: 'billing.subscription-canceled',
|
|
46
|
+
category: 'billing',
|
|
47
|
+
transactional: true,
|
|
48
|
+
subject: { zh: T.zh.subject, en: T.en.subject },
|
|
49
|
+
component: SubscriptionCanceledEmail,
|
|
50
|
+
})
|
|
51
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Section, Text } from '@react-email/components'
|
|
2
|
+
import { EmailLayout } from '../_base/email-layout'
|
|
3
|
+
import { EmailButton } from '../_base/button'
|
|
4
|
+
import { registerTemplate, type EmailLocale } from '../../registry'
|
|
5
|
+
|
|
6
|
+
interface SubscriptionEndingProps {
|
|
7
|
+
locale: EmailLocale
|
|
8
|
+
planName: string
|
|
9
|
+
endDate: string
|
|
10
|
+
dashboardUrl: string
|
|
11
|
+
unsubscribeUrl?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const T = {
|
|
15
|
+
zh: {
|
|
16
|
+
subject: '你的订阅即将到期',
|
|
17
|
+
preview: '订阅即将到期',
|
|
18
|
+
heading: '即将到期',
|
|
19
|
+
body: (plan: string, end: string) => `你的 AllBlue「${plan}」订阅将于 ${end} 到期,请及时续订。`,
|
|
20
|
+
cta: '续订',
|
|
21
|
+
},
|
|
22
|
+
en: {
|
|
23
|
+
subject: 'Your subscription is ending soon',
|
|
24
|
+
preview: 'Subscription ending soon',
|
|
25
|
+
heading: 'Ending soon',
|
|
26
|
+
body: (plan: string, end: string) => `Your AllBlue "${plan}" subscription ends on ${end}. Renew to keep your access.`,
|
|
27
|
+
cta: 'Renew',
|
|
28
|
+
},
|
|
29
|
+
} as const
|
|
30
|
+
|
|
31
|
+
function SubscriptionEndingEmail({ locale, planName, endDate, dashboardUrl, unsubscribeUrl }: SubscriptionEndingProps) {
|
|
32
|
+
const t = T[locale]
|
|
33
|
+
return (
|
|
34
|
+
<EmailLayout locale={locale} preview={t.preview} heading={t.heading} unsubscribeUrl={unsubscribeUrl} footerKind="transactional">
|
|
35
|
+
<Text className="text-base text-fg-2 mx-auto mt-0 mb-8 max-w-[380px] font-sans">{t.body(planName, endDate)}</Text>
|
|
36
|
+
<Section className="mb-6 text-center">
|
|
37
|
+
<EmailButton href={dashboardUrl} label={t.cta} />
|
|
38
|
+
</Section>
|
|
39
|
+
</EmailLayout>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function registerSubscriptionEndingTemplate() {
|
|
44
|
+
registerTemplate({
|
|
45
|
+
key: 'billing.subscription-ending',
|
|
46
|
+
category: 'billing',
|
|
47
|
+
transactional: true,
|
|
48
|
+
subject: { zh: T.zh.subject, en: T.en.subject },
|
|
49
|
+
component: SubscriptionEndingEmail,
|
|
50
|
+
})
|
|
51
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Section, Text } from '@react-email/components'
|
|
2
|
+
import { EmailLayout } from '../_base/email-layout'
|
|
3
|
+
import { EmailButton } from '../_base/button'
|
|
4
|
+
import { registerTemplate, type EmailLocale } from '../../registry'
|
|
5
|
+
|
|
6
|
+
interface NotificationMirrorProps {
|
|
7
|
+
locale: EmailLocale
|
|
8
|
+
title: string
|
|
9
|
+
body?: string
|
|
10
|
+
link?: string
|
|
11
|
+
linkLabel?: string
|
|
12
|
+
unsubscribeUrl?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const T = {
|
|
16
|
+
zh: {
|
|
17
|
+
subject: '你有一条新通知',
|
|
18
|
+
defaultLinkLabel: '查看详情',
|
|
19
|
+
},
|
|
20
|
+
en: {
|
|
21
|
+
subject: 'You have a new notification',
|
|
22
|
+
defaultLinkLabel: 'View details',
|
|
23
|
+
},
|
|
24
|
+
} as const
|
|
25
|
+
|
|
26
|
+
function NotificationMirrorEmail({ locale, title, body, link, linkLabel, unsubscribeUrl }: NotificationMirrorProps) {
|
|
27
|
+
const t = T[locale]
|
|
28
|
+
return (
|
|
29
|
+
<EmailLayout locale={locale} preview={title} heading={title} unsubscribeUrl={unsubscribeUrl} footerKind="notification">
|
|
30
|
+
{body ? (
|
|
31
|
+
<Text className="text-base text-fg-2 mx-auto mt-0 mb-8 max-w-[380px] font-sans">{body}</Text>
|
|
32
|
+
) : null}
|
|
33
|
+
{link ? (
|
|
34
|
+
<Section className="mb-6 text-center">
|
|
35
|
+
<EmailButton href={link} label={linkLabel ?? t.defaultLinkLabel} />
|
|
36
|
+
</Section>
|
|
37
|
+
) : null}
|
|
38
|
+
</EmailLayout>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function registerNotificationMirrorTemplate() {
|
|
43
|
+
registerTemplate({
|
|
44
|
+
key: 'notification.mirror',
|
|
45
|
+
category: 'notification',
|
|
46
|
+
transactional: false,
|
|
47
|
+
subject: { zh: T.zh.subject, en: T.en.subject },
|
|
48
|
+
component: NotificationMirrorEmail,
|
|
49
|
+
})
|
|
50
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Section, Text } from '@react-email/components'
|
|
2
|
+
import { EmailLayout } from '../_base/email-layout'
|
|
3
|
+
import { EmailButton } from '../_base/button'
|
|
4
|
+
import { registerTemplate, type EmailLocale } from '../../registry'
|
|
5
|
+
|
|
6
|
+
interface ChangelogConfirmProps {
|
|
7
|
+
locale: EmailLocale
|
|
8
|
+
confirmUrl: string
|
|
9
|
+
unsubscribeUrl?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const T = {
|
|
13
|
+
zh: {
|
|
14
|
+
subject: '确认订阅 AllBlue 开发更新',
|
|
15
|
+
preview: '请确认订阅 AllBlue 开发更新',
|
|
16
|
+
heading: '确认订阅',
|
|
17
|
+
body: '感谢订阅 AllBlue 开发更新。请点击下方按钮确认订阅(链接 24 小时内有效)。无营销邮件,可随时退订。',
|
|
18
|
+
cta: '确认订阅',
|
|
19
|
+
urlLabel: '如果按钮无法点击,请复制以下链接:',
|
|
20
|
+
},
|
|
21
|
+
en: {
|
|
22
|
+
subject: 'Confirm your AllBlue updates subscription',
|
|
23
|
+
preview: 'Confirm your AllBlue developer updates subscription',
|
|
24
|
+
heading: 'Confirm subscription',
|
|
25
|
+
body: 'Thanks for subscribing to AllBlue developer updates. Click the button below to confirm your subscription (link valid for 24 hours). No marketing emails. Unsubscribe any time.',
|
|
26
|
+
cta: 'Confirm subscription',
|
|
27
|
+
urlLabel: 'If the button does not work, copy this link:',
|
|
28
|
+
},
|
|
29
|
+
} as const
|
|
30
|
+
|
|
31
|
+
function ChangelogConfirmEmail({ locale, confirmUrl, unsubscribeUrl }: ChangelogConfirmProps) {
|
|
32
|
+
const t = T[locale]
|
|
33
|
+
return (
|
|
34
|
+
<EmailLayout locale={locale} preview={t.preview} heading={t.heading} unsubscribeUrl={unsubscribeUrl} footerKind="notification">
|
|
35
|
+
<Text className="text-base text-fg-2 mx-auto mt-0 mb-8 max-w-[380px] font-sans">{t.body}</Text>
|
|
36
|
+
<Section className="mb-6 text-center">
|
|
37
|
+
<EmailButton href={confirmUrl} label={t.cta} />
|
|
38
|
+
</Section>
|
|
39
|
+
<Text className="text-sm text-fg-3 break-all font-sans">{t.urlLabel}</Text>
|
|
40
|
+
<Text className="text-sm text-fg-3 break-all font-sans">{confirmUrl}</Text>
|
|
41
|
+
</EmailLayout>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function registerChangelogConfirmTemplate() {
|
|
46
|
+
registerTemplate({
|
|
47
|
+
key: 'product.changelog-confirm',
|
|
48
|
+
category: 'product',
|
|
49
|
+
transactional: false,
|
|
50
|
+
subject: { zh: T.zh.subject, en: T.en.subject },
|
|
51
|
+
component: ChangelogConfirmEmail,
|
|
52
|
+
})
|
|
53
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Section, Text } from '@react-email/components'
|
|
2
|
+
import { EmailLayout } from '../_base/email-layout'
|
|
3
|
+
import { EmailButton } from '../_base/button'
|
|
4
|
+
import { registerTemplate, type EmailLocale } from '../../registry'
|
|
5
|
+
|
|
6
|
+
interface ChangelogReleaseProps {
|
|
7
|
+
locale: EmailLocale
|
|
8
|
+
version: string
|
|
9
|
+
highlights: string
|
|
10
|
+
detailUrl: string
|
|
11
|
+
unsubscribeUrl?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const T = {
|
|
15
|
+
zh: {
|
|
16
|
+
subject: 'AllBlue v{version} 发布',
|
|
17
|
+
preview: 'AllBlue 新版本发布',
|
|
18
|
+
heading: '新版本发布',
|
|
19
|
+
body: (version: string) => `AllBlue v${version} 已发布。`,
|
|
20
|
+
highlightsLabel: '本次更新要点:',
|
|
21
|
+
cta: '查看更新日志',
|
|
22
|
+
},
|
|
23
|
+
en: {
|
|
24
|
+
subject: 'AllBlue v{version} released',
|
|
25
|
+
preview: 'New AllBlue version released',
|
|
26
|
+
heading: 'New release',
|
|
27
|
+
body: (version: string) => `AllBlue v${version} has been released.`,
|
|
28
|
+
highlightsLabel: 'Highlights:',
|
|
29
|
+
cta: 'View changelog',
|
|
30
|
+
},
|
|
31
|
+
} as const
|
|
32
|
+
|
|
33
|
+
function ChangelogReleaseEmail({ locale, version, highlights, detailUrl, unsubscribeUrl }: ChangelogReleaseProps) {
|
|
34
|
+
const t = T[locale]
|
|
35
|
+
const highlightsList = highlights ? highlights.split('\n').filter(Boolean) : []
|
|
36
|
+
return (
|
|
37
|
+
<EmailLayout locale={locale} preview={t.preview} heading={t.heading} unsubscribeUrl={unsubscribeUrl} footerKind="notification">
|
|
38
|
+
<Text className="text-base text-fg-2 mx-auto mt-0 mb-6 max-w-[380px] font-sans">{t.body(version)}</Text>
|
|
39
|
+
{highlightsList.length > 0 ? (
|
|
40
|
+
<Section className="mb-8">
|
|
41
|
+
<Text className="text-sm text-fg-3 m-0 mb-2 font-sans">{t.highlightsLabel}</Text>
|
|
42
|
+
{highlightsList.map((line, i) => (
|
|
43
|
+
<Text key={i} className="font-14 text-fg-2 m-0 font-sans">· {line}</Text>
|
|
44
|
+
))}
|
|
45
|
+
</Section>
|
|
46
|
+
) : null}
|
|
47
|
+
<Section className="mb-6 text-center">
|
|
48
|
+
<EmailButton href={detailUrl} label={t.cta} />
|
|
49
|
+
</Section>
|
|
50
|
+
</EmailLayout>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function registerChangelogReleaseTemplate() {
|
|
55
|
+
registerTemplate({
|
|
56
|
+
key: 'product.changelog-release',
|
|
57
|
+
category: 'product',
|
|
58
|
+
transactional: false,
|
|
59
|
+
subject: {
|
|
60
|
+
zh: (vars) => `AllBlue v${(vars.version as string) ?? ''} 发布`,
|
|
61
|
+
en: (vars) => `AllBlue v${(vars.version as string) ?? ''} released`,
|
|
62
|
+
},
|
|
63
|
+
component: ChangelogReleaseEmail,
|
|
64
|
+
})
|
|
65
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Section, Text } from '@react-email/components'
|
|
2
|
+
import { EmailLayout } from '../_base/email-layout'
|
|
3
|
+
import { EmailButton } from '../_base/button'
|
|
4
|
+
import { registerTemplate, type EmailLocale } from '../../registry'
|
|
5
|
+
|
|
6
|
+
interface TeamApplyNotifyProps {
|
|
7
|
+
locale: EmailLocale
|
|
8
|
+
applicantEmail: string
|
|
9
|
+
approveUrl: string
|
|
10
|
+
unsubscribeUrl?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const T = {
|
|
14
|
+
zh: {
|
|
15
|
+
subject: '新的团队加入申请',
|
|
16
|
+
preview: '有人申请加入你的团队',
|
|
17
|
+
heading: '加入申请',
|
|
18
|
+
greeting: '你好:',
|
|
19
|
+
body: (email: string) => `有人以邮箱 ${email} 申请加入你的团队,请前往管理页面处理。`,
|
|
20
|
+
cta: '查看申请',
|
|
21
|
+
},
|
|
22
|
+
en: {
|
|
23
|
+
subject: 'New team join request',
|
|
24
|
+
preview: 'Someone requested to join your team',
|
|
25
|
+
heading: 'Join request',
|
|
26
|
+
greeting: 'Hi,',
|
|
27
|
+
body: (email: string) => `Someone with email ${email} requested to join your team. Please review it in the management page.`,
|
|
28
|
+
cta: 'Review request',
|
|
29
|
+
},
|
|
30
|
+
} as const
|
|
31
|
+
|
|
32
|
+
function TeamApplyNotifyEmail({ locale, applicantEmail, approveUrl, unsubscribeUrl }: TeamApplyNotifyProps) {
|
|
33
|
+
const t = T[locale]
|
|
34
|
+
return (
|
|
35
|
+
<EmailLayout locale={locale} preview={t.preview} heading={t.heading} unsubscribeUrl={unsubscribeUrl} footerKind="transactional">
|
|
36
|
+
<Text className="text-base text-fg-2 mt-0 mb-6 font-sans">{t.greeting}</Text>
|
|
37
|
+
<Text className="text-base text-fg-2 mx-auto mt-0 mb-8 max-w-[380px] font-sans">{t.body(applicantEmail)}</Text>
|
|
38
|
+
<Section className="mb-6 text-center">
|
|
39
|
+
<EmailButton href={approveUrl} label={t.cta} />
|
|
40
|
+
</Section>
|
|
41
|
+
</EmailLayout>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function registerTeamApplyNotifyTemplate() {
|
|
46
|
+
registerTemplate({
|
|
47
|
+
key: 'team.apply-notify',
|
|
48
|
+
category: 'team',
|
|
49
|
+
transactional: true,
|
|
50
|
+
subject: { zh: T.zh.subject, en: T.en.subject },
|
|
51
|
+
component: TeamApplyNotifyEmail,
|
|
52
|
+
})
|
|
53
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Section, Text } from '@react-email/components'
|
|
2
|
+
import { EmailLayout } from '../_base/email-layout'
|
|
3
|
+
import { EmailButton } from '../_base/button'
|
|
4
|
+
import { registerTemplate, type EmailLocale } from '../../registry'
|
|
5
|
+
|
|
6
|
+
interface TeamApprovedProps {
|
|
7
|
+
locale: EmailLocale
|
|
8
|
+
teamName?: string
|
|
9
|
+
inviteUrl: string
|
|
10
|
+
unsubscribeUrl?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const T = {
|
|
14
|
+
zh: {
|
|
15
|
+
subject: '你的加入申请已通过',
|
|
16
|
+
preview: '团队申请已批准',
|
|
17
|
+
heading: '申请已通过',
|
|
18
|
+
greeting: '你好:',
|
|
19
|
+
body: (team?: string) => (team ? `你的加入「${team}」团队的申请已通过。` : '你的团队加入申请已通过。'),
|
|
20
|
+
cta: '加入团队',
|
|
21
|
+
},
|
|
22
|
+
en: {
|
|
23
|
+
subject: 'Your join request was approved',
|
|
24
|
+
preview: 'Team request approved',
|
|
25
|
+
heading: 'Request approved',
|
|
26
|
+
greeting: 'Hi,',
|
|
27
|
+
body: (team?: string) => (team ? `Your request to join "${team}" has been approved.` : 'Your team join request has been approved.'),
|
|
28
|
+
cta: 'Join team',
|
|
29
|
+
},
|
|
30
|
+
} as const
|
|
31
|
+
|
|
32
|
+
function TeamApprovedEmail({ locale, teamName, inviteUrl, unsubscribeUrl }: TeamApprovedProps) {
|
|
33
|
+
const t = T[locale]
|
|
34
|
+
return (
|
|
35
|
+
<EmailLayout locale={locale} preview={t.preview} heading={t.heading} unsubscribeUrl={unsubscribeUrl} footerKind="transactional">
|
|
36
|
+
<Text className="text-base text-fg-2 mt-0 mb-6 font-sans">{t.greeting}</Text>
|
|
37
|
+
<Text className="text-base text-fg-2 mx-auto mt-0 mb-8 max-w-[380px] font-sans">{t.body(teamName)}</Text>
|
|
38
|
+
<Section className="mb-6 text-center">
|
|
39
|
+
<EmailButton href={inviteUrl} label={t.cta} />
|
|
40
|
+
</Section>
|
|
41
|
+
</EmailLayout>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function registerTeamApprovedTemplate() {
|
|
46
|
+
registerTemplate({
|
|
47
|
+
key: 'team.approved',
|
|
48
|
+
category: 'team',
|
|
49
|
+
transactional: true,
|
|
50
|
+
subject: { zh: T.zh.subject, en: T.en.subject },
|
|
51
|
+
component: TeamApprovedEmail,
|
|
52
|
+
})
|
|
53
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Section, Text } from '@react-email/components'
|
|
2
|
+
import { EmailLayout } from '../_base/email-layout'
|
|
3
|
+
import { EmailButton } from '../_base/button'
|
|
4
|
+
import { registerTemplate, type EmailLocale } from '../../registry'
|
|
5
|
+
|
|
6
|
+
interface TeamInviteProps {
|
|
7
|
+
locale: EmailLocale
|
|
8
|
+
ownerUsername: string
|
|
9
|
+
inviteUrl: string
|
|
10
|
+
expiresAt: string
|
|
11
|
+
unsubscribeUrl?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const T = {
|
|
15
|
+
zh: {
|
|
16
|
+
subject: '邀请你加入 AllBlue 团队',
|
|
17
|
+
preview: '你收到一个团队邀请',
|
|
18
|
+
heading: '团队邀请',
|
|
19
|
+
greeting: '你好:',
|
|
20
|
+
body: (owner: string) => `${owner || '团队成员'} 邀请你加入 AllBlue 团队。点击下方按钮接受邀请(${'过期时间见下'})。`,
|
|
21
|
+
cta: '接受邀请',
|
|
22
|
+
expiresLabel: '有效期至:',
|
|
23
|
+
},
|
|
24
|
+
en: {
|
|
25
|
+
subject: 'You are invited to an AllBlue team',
|
|
26
|
+
preview: 'You received a team invitation',
|
|
27
|
+
heading: 'Team invitation',
|
|
28
|
+
greeting: 'Hi,',
|
|
29
|
+
body: (owner: string) => `${owner || 'A team member'} invited you to join an AllBlue team. Click the button below to accept.`,
|
|
30
|
+
cta: 'Accept invite',
|
|
31
|
+
expiresLabel: 'Valid until:',
|
|
32
|
+
},
|
|
33
|
+
} as const
|
|
34
|
+
|
|
35
|
+
function TeamInviteEmail({ locale, ownerUsername, inviteUrl, expiresAt, unsubscribeUrl }: TeamInviteProps) {
|
|
36
|
+
const t = T[locale]
|
|
37
|
+
return (
|
|
38
|
+
<EmailLayout locale={locale} preview={t.preview} heading={t.heading} unsubscribeUrl={unsubscribeUrl} footerKind="transactional">
|
|
39
|
+
<Text className="text-base text-fg-2 mt-0 mb-6 font-sans">{t.greeting}</Text>
|
|
40
|
+
<Text className="text-base text-fg-2 mx-auto mt-0 mb-8 max-w-[380px] font-sans">{t.body(ownerUsername)}</Text>
|
|
41
|
+
<Section className="mb-6 text-center">
|
|
42
|
+
<EmailButton href={inviteUrl} label={t.cta} />
|
|
43
|
+
</Section>
|
|
44
|
+
<Text className="text-sm text-fg-3 font-sans">{t.expiresLabel} {expiresAt}</Text>
|
|
45
|
+
</EmailLayout>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function registerTeamInviteTemplate() {
|
|
50
|
+
registerTemplate({
|
|
51
|
+
key: 'team.invite',
|
|
52
|
+
category: 'team',
|
|
53
|
+
transactional: true,
|
|
54
|
+
subject: { zh: T.zh.subject, en: T.en.subject },
|
|
55
|
+
component: TeamInviteEmail,
|
|
56
|
+
})
|
|
57
|
+
}
|