mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-04 21:04:37 +08:00
Email templates and project logos (#852)
https://www.loom.com/share/7628a0b5f14e4367bcde93e4817a50e8 <img width="811" height="437" alt="image" src="https://github.com/user-attachments/assets/6b02aae0-c723-43d4-92ab-a26e97623d9c" /> <!-- Make sure you've read the CONTRIBUTING.md guidelines: https://github.com/stack-auth/stack-auth/blob/dev/CONTRIBUTING.md --> <!-- ELLIPSIS_HIDDEN --> ---- > [!IMPORTANT] > Enhance email templates with project branding and dark-mode support, update schemas and API responses for new logo fields. > > - **Behavior**: > - Email templates now support project branding with compact/full logos and dark-mode variants in `email-rendering.tsx` and `emails.ts`. > - Project name displays beside logos in Light and Dark themes in `email-rendering.tsx`. > - Email rendering groups branding and unsubscribe data for consistent theming. > - **API Changes**: > - Project payloads and admin/project schemas rename `full_logo` to `logo_full` and add dark-mode logo fields in `projects.ts` and `schema-fields.ts`. > - Updates to `projects.tsx` and `page-client.tsx` to handle new logo fields. > - **Style**: > - Improved unsubscribe link contrast in Dark theme in `emails.ts`. > - **Database**: > - Adds `logoFullUrl`, `logoDarkModeUrl`, and `logoFullDarkModeUrl` columns to `Project` table in `migration.sql`. > > <sup>This description was created by </sup>[<img alt="Ellipsis" src="https://img.shields.io/badge/Ellipsis-blue?color=175173">](https://www.ellipsis.dev?ref=stack-auth%2Fstack-auth&utm_source=github&utm_medium=referral)<sup> for0fbb79db5c. You can [customize](https://app.ellipsis.dev/stack-auth/settings/summaries) this summary. It will automatically update as commits are pushed.</sup> ---- <!-- ELLIPSIS_HIDDEN --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added dark‑mode logo support for projects with separate logo and full‑logo variants. * Email themes/templates now render project logos and automatically select light/dark variants with fallbacks. * **Refactor** * Project logo fields and public payloads renamed/reorganized to support the new dark‑mode variants and consistent naming. * **Tests** * Updated test snapshots to reflect the new project logo fields. * **Chores** * Database migration applied to add/rename logo columns. <!-- end of auto-generated comment: release notes by coderabbit.ai --> <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Adds light/dark logo variants, renames full-logo fields, and passes project logos into email themes; updates DB, schemas, API, UI, and tests. > > - **Database/Prisma**: > - Rename `Project.fullLogoUrl` to `logoFullUrl`; add `logoDarkModeUrl` and `logoFullDarkModeUrl`. > - **Backend**: > - Email rendering now accepts `themeProps` with `projectLogos` and spreads into `EmailTheme` (single/batched). > - Provide project logo URLs to email render/send and internal template preview routes. > - Add `@stackframe/emails` components (`Logo`, `FullLogo`, `ProjectLogo`) with light/dark fallbacks. > - Projects CRUD: map/upload new logo fields; rename API fields to `logo_full_url` and add dark-mode fields. > - **Emails (themes)**: > - Light/Dark themes render `<ProjectLogo>`; improve dark-theme unsubscribe link contrast. > - **Dashboard/UI**: > - Project settings support `logoFullUrl`, `logoDarkModeUrl`, `logoFullDarkModeUrl` uploads. > - Code editor types: add `ThemeProps.projectLogos`, relax TS option, and Tailwind DTS fix. > - **Shared Schemas/Types**: > - Update `schema-fields` and CRUD read/update models to new/extra logo fields; propagate through template app types. > - **Tests**: > - Update snapshots for new project logo fields and theme source output. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit4d97561839. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> Co-authored-by: Konsti Wohlwend <n2d4xc@gmail.com> Co-authored-by: Bilal Godil <bg2002@gmail.com>
This commit is contained in:
parent
49ce3c0cc7
commit
4b955ced3e
@ -0,0 +1,7 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Project"
|
||||
RENAME COLUMN "fullLogoUrl" TO "logoFullUrl";
|
||||
|
||||
ALTER TABLE "Project"
|
||||
ADD COLUMN "logoDarkModeUrl" TEXT,
|
||||
ADD COLUMN "logoFullDarkModeUrl" TEXT;
|
||||
@ -30,8 +30,11 @@ model Project {
|
||||
description String @default("")
|
||||
isProductionMode Boolean
|
||||
ownerTeamId String? @db.Uuid
|
||||
logoUrl String?
|
||||
fullLogoUrl String?
|
||||
|
||||
logoUrl String?
|
||||
logoFullUrl String?
|
||||
logoDarkModeUrl String?
|
||||
logoFullDarkModeUrl String?
|
||||
|
||||
projectConfigOverride Json?
|
||||
stripeAccountId String?
|
||||
|
||||
@ -75,6 +75,14 @@ export const POST = createSmartRouteHandler({
|
||||
{
|
||||
project: { displayName: tenancy.project.display_name },
|
||||
previewMode: true,
|
||||
themeProps: {
|
||||
projectLogos: {
|
||||
logoUrl: tenancy.project.logo_url ?? undefined,
|
||||
logoFullUrl: tenancy.project.logo_full_url ?? undefined,
|
||||
logoDarkModeUrl: tenancy.project.logo_dark_mode_url ?? undefined,
|
||||
logoFullDarkModeUrl: tenancy.project.logo_full_dark_mode_url ?? undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
if (result.status === "error") {
|
||||
|
||||
@ -195,6 +195,14 @@ export const POST = createSmartRouteHandler({
|
||||
project: { displayName: auth.tenancy.project.display_name },
|
||||
variables,
|
||||
unsubscribeLink: unsubLinks.get(user.projectUserId),
|
||||
themeProps: {
|
||||
projectLogos: {
|
||||
logoUrl: auth.tenancy.project.logo_url ?? undefined,
|
||||
logoFullUrl: auth.tenancy.project.logo_full_url ?? undefined,
|
||||
logoDarkModeUrl: auth.tenancy.project.logo_dark_mode_url ?? undefined,
|
||||
logoFullDarkModeUrl: auth.tenancy.project.logo_full_dark_mode_url ?? undefined,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const inputChunks = getChunks(finalInputs, BATCH_SIZE);
|
||||
|
||||
@ -42,6 +42,14 @@ export const PATCH = createSmartRouteHandler({
|
||||
const result = await renderEmailWithTemplate(body.tsx_source, theme.tsxSource, {
|
||||
variables: { projectDisplayName: tenancy.project.display_name },
|
||||
previewMode: true,
|
||||
themeProps: {
|
||||
projectLogos: {
|
||||
logoUrl: tenancy.project.logo_url ?? undefined,
|
||||
logoFullUrl: tenancy.project.logo_full_url ?? undefined,
|
||||
logoDarkModeUrl: tenancy.project.logo_dark_mode_url ?? undefined,
|
||||
logoFullDarkModeUrl: tenancy.project.logo_full_dark_mode_url ?? undefined,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (result.status === "error") {
|
||||
throw new KnownErrors.EmailRenderingError(result.error);
|
||||
|
||||
@ -49,7 +49,15 @@ export async function renderEmailWithTemplate(
|
||||
user?: { displayName: string | null },
|
||||
project?: { displayName: string },
|
||||
variables?: Record<string, any>,
|
||||
unsubscribeLink?: string,
|
||||
themeProps?: {
|
||||
unsubscribeLink?: string,
|
||||
projectLogos: {
|
||||
logoUrl?: string,
|
||||
logoFullUrl?: string,
|
||||
logoDarkModeUrl?: string,
|
||||
logoFullDarkModeUrl?: string,
|
||||
},
|
||||
},
|
||||
previewMode?: boolean,
|
||||
},
|
||||
): Promise<Result<{ html: string, text: string, subject?: string, notificationCategory?: string }, string>> {
|
||||
@ -86,9 +94,12 @@ export async function renderEmailWithTemplate(
|
||||
if (variables instanceof type.errors) {
|
||||
throw new Error(variables.summary)
|
||||
}
|
||||
const unsubscribeLink = ${previewMode ? "EmailTheme.PreviewProps?.unsubscribeLink" : JSON.stringify(options.unsubscribeLink)};
|
||||
const themeProps = {
|
||||
...${JSON.stringify(options.themeProps || {})},
|
||||
...${previewMode ? "EmailTheme.PreviewProps" : "{}"},
|
||||
}
|
||||
const EmailTemplateWithProps = <EmailTemplate variables={variables} user={${JSON.stringify(user)}} project={${JSON.stringify(project)}} />;
|
||||
const Email = <EmailTheme unsubscribeLink={unsubscribeLink}>
|
||||
const Email = <EmailTheme {...themeProps}>
|
||||
{${previewMode ? "EmailTheme.PreviewProps?.children ?? " : ""} EmailTemplateWithProps}
|
||||
</EmailTheme>;
|
||||
return {
|
||||
@ -145,6 +156,14 @@ export async function renderEmailsWithTemplateBatched(
|
||||
project: { displayName: string },
|
||||
variables?: Record<string, any>,
|
||||
unsubscribeLink?: string,
|
||||
themeProps?: {
|
||||
projectLogos: {
|
||||
logoUrl?: string,
|
||||
logoFullUrl?: string,
|
||||
logoDarkModeUrl?: string,
|
||||
logoFullDarkModeUrl?: string,
|
||||
},
|
||||
},
|
||||
}>,
|
||||
): Promise<Result<Array<{ html: string, text: string, subject?: string, notificationCategory?: string }>, string>> {
|
||||
const apiKey = getEnvVariable("STACK_FREESTYLE_API_KEY");
|
||||
@ -175,8 +194,12 @@ export async function renderEmailsWithTemplateBatched(
|
||||
if (variables instanceof type.errors) {
|
||||
throw new Error(variables.summary)
|
||||
}
|
||||
const themeProps = {
|
||||
...{ projectLogos: input.themeProps?.projectLogos ?? {} },
|
||||
unsubscribeLink: input.unsubscribeLink,
|
||||
}
|
||||
const EmailTemplateWithProps = <EmailTemplate variables={variables} user={input.user} project={input.project} />;
|
||||
const Email = <EmailTheme unsubscribeLink={input.unsubscribeLink}>
|
||||
const Email = <EmailTheme {...themeProps}>
|
||||
{ EmailTemplateWithProps }
|
||||
</EmailTheme>;
|
||||
return {
|
||||
@ -276,9 +299,61 @@ export function findComponentValue(element, targetStackComponent) {
|
||||
return value;
|
||||
}`;
|
||||
|
||||
// issues with using jsx in external packages, using React.createElement instead
|
||||
const stackframeEmailsPackage = deindent`
|
||||
import React from 'react';
|
||||
import { Img } from '@react-email/components';
|
||||
export const Subject = (props) => null;
|
||||
Subject.__stackComponent = "Subject";
|
||||
export const NotificationCategory = (props) => null;
|
||||
NotificationCategory.__stackComponent = "NotificationCategory";
|
||||
|
||||
export function Logo(props) {
|
||||
return React.createElement(
|
||||
"div",
|
||||
{ className: "flex gap-2 items-center" },
|
||||
React.createElement(Img, {
|
||||
src: props.logoUrl,
|
||||
alt: "Logo",
|
||||
className: "h-8",
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function FullLogo(props) {
|
||||
return React.createElement(Img, {
|
||||
src: props.logoFullUrl,
|
||||
alt: "Full Logo",
|
||||
className: "h-16",
|
||||
});
|
||||
}
|
||||
|
||||
export function ProjectLogo(props) {
|
||||
const { mode = "light" } = props;
|
||||
const {
|
||||
logoUrl,
|
||||
logoFullUrl,
|
||||
logoDarkModeUrl,
|
||||
logoFullDarkModeUrl,
|
||||
} = props.data ?? {};
|
||||
|
||||
if (mode === "dark" && logoFullDarkModeUrl) {
|
||||
return React.createElement(FullLogo, { logoFullUrl: logoFullDarkModeUrl });
|
||||
}
|
||||
if (mode === "dark" && logoDarkModeUrl) {
|
||||
return React.createElement(Logo, {
|
||||
logoUrl: logoDarkModeUrl,
|
||||
});
|
||||
}
|
||||
if (mode === "light" && logoFullUrl) {
|
||||
return React.createElement(FullLogo, { logoFullUrl });
|
||||
}
|
||||
if (mode === "light" && logoUrl) {
|
||||
return React.createElement(Logo, {
|
||||
logoUrl,
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
`;
|
||||
|
||||
@ -398,6 +398,14 @@ export async function sendEmailFromTemplate(options: {
|
||||
user: { displayName: options.user?.display_name ?? null },
|
||||
project: { displayName: options.tenancy.project.display_name },
|
||||
variables,
|
||||
themeProps: {
|
||||
projectLogos: {
|
||||
logoUrl: options.tenancy.project.logo_url ?? undefined,
|
||||
logoFullUrl: options.tenancy.project.logo_full_url ?? undefined,
|
||||
logoDarkModeUrl: options.tenancy.project.logo_dark_mode_url ?? undefined,
|
||||
logoFullDarkModeUrl: options.tenancy.project.logo_full_dark_mode_url ?? undefined,
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
if (result.status === 'error') {
|
||||
|
||||
@ -59,7 +59,9 @@ export function getProjectQuery(projectId: string): RawQuery<Promise<Omit<Projec
|
||||
display_name: row.displayName,
|
||||
description: row.description,
|
||||
logo_url: row.logoUrl,
|
||||
full_logo_url: row.fullLogoUrl,
|
||||
logo_full_url: row.logoFullUrl,
|
||||
logo_dark_mode_url: row.logoDarkModeUrl,
|
||||
logo_full_dark_mode_url: row.logoFullDarkModeUrl,
|
||||
created_at_millis: new Date(row.createdAt + "Z").getTime(),
|
||||
is_production_mode: row.isProductionMode,
|
||||
owner_team_id: row.ownerTeamId,
|
||||
@ -88,14 +90,13 @@ export async function createOrUpdateProjectWithLegacyConfig(
|
||||
data: ProjectsCrud["Admin"]["Update"],
|
||||
})
|
||||
) {
|
||||
let logoUrl: string | null | undefined;
|
||||
if (options.data.logo_url !== undefined) {
|
||||
logoUrl = await uploadAndGetUrl(options.data.logo_url, "project-logos");
|
||||
}
|
||||
const logoFields = ['logo_url', 'logo_full_url', 'logo_dark_mode_url', 'logo_full_dark_mode_url'] as const;
|
||||
const logoUrls: Record<string, string | null | undefined> = {};
|
||||
|
||||
let fullLogoUrl: string | null | undefined;
|
||||
if (options.data.full_logo_url !== undefined) {
|
||||
fullLogoUrl = await uploadAndGetUrl(options.data.full_logo_url, "project-logos");
|
||||
for (const field of logoFields) {
|
||||
if (options.data[field] !== undefined) {
|
||||
logoUrls[field] = await uploadAndGetUrl(options.data[field], "project-logos");
|
||||
}
|
||||
}
|
||||
|
||||
const [projectId, branchId] = await retryTransaction(globalPrismaClient, async (tx) => {
|
||||
@ -110,8 +111,10 @@ export async function createOrUpdateProjectWithLegacyConfig(
|
||||
description: options.data.description ?? "",
|
||||
isProductionMode: options.data.is_production_mode ?? false,
|
||||
ownerTeamId: options.data.owner_team_id,
|
||||
logoUrl,
|
||||
fullLogoUrl,
|
||||
logoUrl: logoUrls['logo_url'],
|
||||
logoFullUrl: logoUrls['logo_full_url'],
|
||||
logoDarkModeUrl: logoUrls['logo_dark_mode_url'],
|
||||
logoFullDarkModeUrl: logoUrls['logo_full_dark_mode_url'],
|
||||
},
|
||||
});
|
||||
|
||||
@ -142,8 +145,10 @@ export async function createOrUpdateProjectWithLegacyConfig(
|
||||
displayName: options.data.display_name,
|
||||
description: options.data.description === null ? "" : options.data.description,
|
||||
isProductionMode: options.data.is_production_mode,
|
||||
logoUrl,
|
||||
fullLogoUrl,
|
||||
logoUrl: logoUrls['logo_url'],
|
||||
logoFullUrl: logoUrls['logo_full_url'],
|
||||
logoDarkModeUrl: logoUrls['logo_dark_mode_url'],
|
||||
logoFullDarkModeUrl: logoUrls['logo_full_dark_mode_url'],
|
||||
},
|
||||
});
|
||||
branchId = options.branchId;
|
||||
|
||||
@ -130,14 +130,34 @@ export default function PageClient() {
|
||||
|
||||
<LogoUpload
|
||||
label="Full Logo"
|
||||
value={project.fullLogoUrl}
|
||||
onValueChange={async (fullLogoUrl) => {
|
||||
await project.update({ fullLogoUrl });
|
||||
value={project.logoFullUrl}
|
||||
onValueChange={async (logoFullUrl) => {
|
||||
await project.update({ logoFullUrl });
|
||||
}}
|
||||
description="Upload a full logo with text. Recommended size: At least 100px tall, landscape format"
|
||||
type="full-logo"
|
||||
/>
|
||||
|
||||
<LogoUpload
|
||||
label="Logo (Dark Mode)"
|
||||
value={project.logoDarkModeUrl}
|
||||
onValueChange={async (logoDarkModeUrl) => {
|
||||
await project.update({ logoDarkModeUrl });
|
||||
}}
|
||||
description="Upload a dark mode version of your logo. Recommended size: 200x200px"
|
||||
type="logo"
|
||||
/>
|
||||
|
||||
<LogoUpload
|
||||
label="Full Logo (Dark Mode)"
|
||||
value={project.logoFullDarkModeUrl}
|
||||
onValueChange={async (logoFullDarkModeUrl) => {
|
||||
await project.update({ logoFullDarkModeUrl });
|
||||
}}
|
||||
description="Upload a dark mode version of your full logo. Recommended size: At least 100px tall, landscape format"
|
||||
type="full-logo"
|
||||
/>
|
||||
|
||||
<Typography variant="secondary" type="footnote">
|
||||
Logo images will be displayed in your application (e.g. login page) and emails. The logo should be a square image, while the full logo can include text and be wider.
|
||||
</Typography>
|
||||
|
||||
@ -46,6 +46,7 @@ export default function CodeEditor({
|
||||
skipLibCheck: true,
|
||||
strict: true,
|
||||
strictNullChecks: true,
|
||||
strictFunctionTypes: false,
|
||||
exactOptionalPropertyTypes: true,
|
||||
});
|
||||
runAsynchronously(addTypeFiles(monaco));
|
||||
@ -55,12 +56,14 @@ export default function CodeEditor({
|
||||
monaco: Monaco,
|
||||
moduleName: string,
|
||||
url: string,
|
||||
transform?: (content: string) => string,
|
||||
) => {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (response.ok) {
|
||||
const content = await response.text();
|
||||
monaco.languages.typescript.typescriptDefaults.addExtraLib(content, `file:///node_modules/${moduleName}/index.d.ts`);
|
||||
const transformed = transform ? transform(content) : content;
|
||||
monaco.languages.typescript.typescriptDefaults.addExtraLib(transformed, `file:///node_modules/${moduleName}/index.d.ts`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Failed to fetch type definitions from ${url}:`, error);
|
||||
@ -93,7 +96,14 @@ export default function CodeEditor({
|
||||
type ThemeProps = {
|
||||
children: React.ReactNode;
|
||||
unsubscribeLink?: string;
|
||||
projectLogos: {
|
||||
logoUrl?: string;
|
||||
logoFullUrl?: string;
|
||||
logoDarkModeUrl?: string;
|
||||
logoFullDarkModeUrl?: string;
|
||||
};
|
||||
};
|
||||
const ProjectLogo: React.FC<{data: ThemeProps['projectLogos'], mode: 'light' | 'dark'}>;
|
||||
}
|
||||
`,
|
||||
);
|
||||
@ -104,18 +114,30 @@ export default function CodeEditor({
|
||||
const reactEmailPackages = [
|
||||
'components', 'body', 'button', 'code-block', 'code-inline', 'column',
|
||||
'container', 'font', 'head', 'heading', 'hr', 'html', 'img', 'link',
|
||||
'markdown', 'preview', 'render', 'row', 'section', 'tailwind', 'text'
|
||||
'markdown', 'preview', 'row', 'section', 'tailwind', 'text'
|
||||
];
|
||||
await Promise.all([
|
||||
// latest version of react causes type issue with rendering react-email components
|
||||
fetchAndAddTypeDefinition(monaco, 'react', 'https://unpkg.com/@types/react@18.0.38/index.d.ts'),
|
||||
fetchAndAddTypeDefinition(monaco, 'csstype', 'https://unpkg.com/csstype@3.1.3/index.d.ts'),
|
||||
...reactEmailPackages.map(packageName =>
|
||||
fetchAndAddTypeDefinition(monaco, `@react-email/${packageName}`, `https://unpkg.com/@react-email/${packageName}/dist/index.d.ts`)
|
||||
fetchAndAddTypeDefinition(
|
||||
monaco,
|
||||
`@react-email/${packageName}`,
|
||||
`https://unpkg.com/@react-email/${packageName}/dist/index.d.ts`,
|
||||
packageName === "tailwind" ? transformTailwindTypeFile : undefined
|
||||
)
|
||||
),
|
||||
]);
|
||||
};
|
||||
|
||||
const transformTailwindTypeFile = (content: string) => {
|
||||
return content.replace(
|
||||
/}\s*:\s*TailwindProps\)\s*:\s*React\.ReactNode;/,
|
||||
'}: TailwindProps): JSX.Element | null;'
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="p-3 flex justify-between items-center">
|
||||
|
||||
@ -92,15 +92,16 @@ describe("get email theme", () => {
|
||||
"display_name": "Default Light",
|
||||
"tsx_source": deindent\`
|
||||
import { Html, Head, Tailwind, Body, Container, Link } from '@react-email/components';
|
||||
import { ThemeProps } from "@stackframe/emails"
|
||||
import { ThemeProps, ProjectLogo } from "@stackframe/emails";
|
||||
|
||||
export function EmailTheme({ children, unsubscribeLink }: ThemeProps) {
|
||||
export function EmailTheme({ children, unsubscribeLink, projectLogos }: ThemeProps) {
|
||||
return (
|
||||
<Html>
|
||||
<Head />
|
||||
<Tailwind>
|
||||
<Body className="bg-[#fafbfb] font-sans text-base">
|
||||
<Container className="bg-white p-[45px] rounded-lg">
|
||||
<ProjectLogo data={projectLogos} mode="light" />
|
||||
{children}
|
||||
</Container>
|
||||
{unsubscribeLink && (
|
||||
@ -116,7 +117,7 @@ describe("get email theme", () => {
|
||||
}
|
||||
|
||||
EmailTheme.PreviewProps = {
|
||||
unsubscribeLink: "https://example.com"
|
||||
unsubscribeLink: "https://example.com",
|
||||
} satisfies Partial<ThemeProps>
|
||||
\` + "\\n",
|
||||
},
|
||||
|
||||
@ -80,9 +80,11 @@ it("should be able to provision a new project if client details are correct", as
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "Project created by an external integration",
|
||||
"display_name": "Test project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_dark_mode_url": null,
|
||||
"logo_full_dark_mode_url": null,
|
||||
"logo_full_url": null,
|
||||
"logo_url": null,
|
||||
"owner_team_id": null,
|
||||
},
|
||||
|
||||
@ -126,9 +126,11 @@ it("lists oauth providers", async ({ expect }) => {
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "New Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_dark_mode_url": null,
|
||||
"logo_full_dark_mode_url": null,
|
||||
"logo_full_url": null,
|
||||
"logo_url": null,
|
||||
"owner_team_id": "<stripped UUID>",
|
||||
},
|
||||
|
||||
@ -39,9 +39,11 @@ it("get project details", async ({ expect }) => {
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "New Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_dark_mode_url": null,
|
||||
"logo_full_dark_mode_url": null,
|
||||
"logo_full_url": null,
|
||||
"logo_url": null,
|
||||
"owner_team_id": "<stripped UUID>",
|
||||
},
|
||||
@ -94,9 +96,11 @@ it("creates and updates the basic project information of a project", async ({ ex
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "Updated description",
|
||||
"display_name": "Updated Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": true,
|
||||
"logo_dark_mode_url": null,
|
||||
"logo_full_dark_mode_url": null,
|
||||
"logo_full_url": null,
|
||||
"logo_url": null,
|
||||
"owner_team_id": "<stripped UUID>",
|
||||
},
|
||||
@ -165,9 +169,11 @@ it("creates and updates the email config of a project", async ({ expect }) => {
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "New Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_dark_mode_url": null,
|
||||
"logo_full_dark_mode_url": null,
|
||||
"logo_full_url": null,
|
||||
"logo_url": null,
|
||||
"owner_team_id": "<stripped UUID>",
|
||||
},
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { decryptValue, hashKey } from "@stackframe/stack-shared/dist/helpers/vault/client-side";
|
||||
import { it } from "../../../../../../../helpers";
|
||||
import { Auth, InternalApiKey, InternalProjectKeys, Project, backendContext, niceBackendFetch } from "../../../../../../backend-helpers";
|
||||
import { Auth, InternalApiKey, InternalProjectKeys, backendContext, niceBackendFetch } from "../../../../../../backend-helpers";
|
||||
|
||||
export async function provisionProject() {
|
||||
return await niceBackendFetch("/api/v1/integrations/neon/projects/provision", {
|
||||
@ -81,9 +81,11 @@ it("should be able to provision a new project if neon client details are correct
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "Created with Neon",
|
||||
"display_name": "Test project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_dark_mode_url": null,
|
||||
"logo_full_dark_mode_url": null,
|
||||
"logo_full_url": null,
|
||||
"logo_url": null,
|
||||
"owner_team_id": null,
|
||||
},
|
||||
|
||||
@ -92,9 +92,11 @@ it("creates a new project", async ({ expect }) => {
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "Test Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_dark_mode_url": null,
|
||||
"logo_full_dark_mode_url": null,
|
||||
"logo_full_url": null,
|
||||
"logo_url": null,
|
||||
"owner_team_id": "<stripped UUID>",
|
||||
},
|
||||
@ -145,9 +147,11 @@ it("creates a new project with different configurations", async ({ expect }) =>
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "Test description",
|
||||
"display_name": "Test Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": true,
|
||||
"logo_dark_mode_url": null,
|
||||
"logo_full_dark_mode_url": null,
|
||||
"logo_full_url": null,
|
||||
"logo_url": null,
|
||||
"owner_team_id": "<stripped UUID>",
|
||||
},
|
||||
@ -201,9 +205,11 @@ it("creates a new project with different configurations", async ({ expect }) =>
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "Test Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_dark_mode_url": null,
|
||||
"logo_full_dark_mode_url": null,
|
||||
"logo_full_url": null,
|
||||
"logo_url": null,
|
||||
"owner_team_id": "<stripped UUID>",
|
||||
},
|
||||
@ -248,9 +254,11 @@ it("creates a new project with different configurations", async ({ expect }) =>
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "Test Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_dark_mode_url": null,
|
||||
"logo_full_dark_mode_url": null,
|
||||
"logo_full_url": null,
|
||||
"logo_url": null,
|
||||
"owner_team_id": "<stripped UUID>",
|
||||
},
|
||||
@ -309,9 +317,11 @@ it("creates a new project with different configurations", async ({ expect }) =>
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "Test Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_dark_mode_url": null,
|
||||
"logo_full_dark_mode_url": null,
|
||||
"logo_full_url": null,
|
||||
"logo_url": null,
|
||||
"owner_team_id": "<stripped UUID>",
|
||||
},
|
||||
@ -372,9 +382,11 @@ it("creates a new project with different configurations", async ({ expect }) =>
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "Test Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_dark_mode_url": null,
|
||||
"logo_full_dark_mode_url": null,
|
||||
"logo_full_url": null,
|
||||
"logo_url": null,
|
||||
"owner_team_id": "<stripped UUID>",
|
||||
},
|
||||
@ -418,9 +430,11 @@ it("lists the current projects after creating a new project", async ({ expect })
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "New Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_dark_mode_url": null,
|
||||
"logo_full_dark_mode_url": null,
|
||||
"logo_full_url": null,
|
||||
"logo_url": null,
|
||||
"owner_team_id": "<stripped UUID>",
|
||||
},
|
||||
@ -470,9 +484,11 @@ it("verifies email_theme update persists", async ({ expect }) => {
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "New Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_dark_mode_url": null,
|
||||
"logo_full_dark_mode_url": null,
|
||||
"logo_full_url": null,
|
||||
"logo_url": null,
|
||||
"owner_team_id": "<stripped UUID>",
|
||||
},
|
||||
@ -511,9 +527,11 @@ it("verifies email_theme update persists", async ({ expect }) => {
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "New Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_dark_mode_url": null,
|
||||
"logo_full_dark_mode_url": null,
|
||||
"logo_full_url": null,
|
||||
"logo_url": null,
|
||||
"owner_team_id": "<stripped UUID>",
|
||||
},
|
||||
@ -562,9 +580,11 @@ it("updates trusted domains without modifying allow_localhost", async ({ expect
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "New Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_dark_mode_url": null,
|
||||
"logo_full_dark_mode_url": null,
|
||||
"logo_full_url": null,
|
||||
"logo_url": null,
|
||||
"owner_team_id": "<stripped UUID>",
|
||||
},
|
||||
@ -616,9 +636,11 @@ it("updates trusted domains without modifying allow_localhost", async ({ expect
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "New Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_dark_mode_url": null,
|
||||
"logo_full_dark_mode_url": null,
|
||||
"logo_full_url": null,
|
||||
"logo_url": null,
|
||||
"owner_team_id": "<stripped UUID>",
|
||||
},
|
||||
@ -687,9 +709,11 @@ it("lets user update logo_url to a valid image", async ({ expect }) => {
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "New Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_dark_mode_url": null,
|
||||
"logo_full_dark_mode_url": null,
|
||||
"logo_full_url": null,
|
||||
"logo_url": "http://localhost:<$NEXT_PUBLIC_STACK_PORT_PREFIX>21/stack-storage/project-logos/<stripped UUID>.png",
|
||||
"owner_team_id": "<stripped UUID>",
|
||||
},
|
||||
|
||||
@ -237,9 +237,11 @@ it("can customize default user permissions", async ({ expect }) => {
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "New Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_dark_mode_url": null,
|
||||
"logo_full_dark_mode_url": null,
|
||||
"logo_full_url": null,
|
||||
"logo_url": null,
|
||||
"owner_team_id": "<stripped UUID>",
|
||||
},
|
||||
|
||||
@ -98,9 +98,11 @@ it("creates and updates the basic project information of a project", async ({ ex
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "Updated description",
|
||||
"display_name": "Updated Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": true,
|
||||
"logo_dark_mode_url": null,
|
||||
"logo_full_dark_mode_url": null,
|
||||
"logo_full_url": null,
|
||||
"logo_url": null,
|
||||
"owner_team_id": "<stripped UUID>",
|
||||
},
|
||||
@ -148,9 +150,11 @@ it("updates the basic project configuration", async ({ expect }) => {
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "New Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_dark_mode_url": null,
|
||||
"logo_full_dark_mode_url": null,
|
||||
"logo_full_url": null,
|
||||
"logo_url": null,
|
||||
"owner_team_id": "<stripped UUID>",
|
||||
},
|
||||
@ -203,9 +207,11 @@ it("updates the project domains configuration", async ({ expect }) => {
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "New Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_dark_mode_url": null,
|
||||
"logo_full_dark_mode_url": null,
|
||||
"logo_full_url": null,
|
||||
"logo_url": null,
|
||||
"owner_team_id": "<stripped UUID>",
|
||||
},
|
||||
@ -264,9 +270,11 @@ it("updates the project domains configuration", async ({ expect }) => {
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "New Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_dark_mode_url": null,
|
||||
"logo_full_dark_mode_url": null,
|
||||
"logo_full_url": null,
|
||||
"logo_url": null,
|
||||
"owner_team_id": "<stripped UUID>",
|
||||
},
|
||||
@ -319,9 +327,11 @@ it("should allow insecure HTTP connections if insecureHttp is true", async ({ ex
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "New Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_dark_mode_url": null,
|
||||
"logo_full_dark_mode_url": null,
|
||||
"logo_full_url": null,
|
||||
"logo_url": null,
|
||||
"owner_team_id": "<stripped UUID>",
|
||||
},
|
||||
@ -417,9 +427,11 @@ it("updates the project email configuration", async ({ expect }) => {
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "New Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_dark_mode_url": null,
|
||||
"logo_full_dark_mode_url": null,
|
||||
"logo_full_url": null,
|
||||
"logo_url": null,
|
||||
"owner_team_id": "<stripped UUID>",
|
||||
},
|
||||
@ -477,9 +489,11 @@ it("updates the project email configuration", async ({ expect }) => {
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "New Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_dark_mode_url": null,
|
||||
"logo_full_dark_mode_url": null,
|
||||
"logo_full_url": null,
|
||||
"logo_url": null,
|
||||
"owner_team_id": "<stripped UUID>",
|
||||
},
|
||||
@ -523,9 +537,11 @@ it("updates the project email configuration", async ({ expect }) => {
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "New Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_dark_mode_url": null,
|
||||
"logo_full_dark_mode_url": null,
|
||||
"logo_full_url": null,
|
||||
"logo_url": null,
|
||||
"owner_team_id": "<stripped UUID>",
|
||||
},
|
||||
@ -569,9 +585,11 @@ it("updates the project email configuration", async ({ expect }) => {
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "New Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_dark_mode_url": null,
|
||||
"logo_full_dark_mode_url": null,
|
||||
"logo_full_url": null,
|
||||
"logo_url": null,
|
||||
"owner_team_id": "<stripped UUID>",
|
||||
},
|
||||
@ -629,9 +647,11 @@ it("updates the project email configuration", async ({ expect }) => {
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "New Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_dark_mode_url": null,
|
||||
"logo_full_dark_mode_url": null,
|
||||
"logo_full_url": null,
|
||||
"logo_url": null,
|
||||
"owner_team_id": "<stripped UUID>",
|
||||
},
|
||||
@ -802,9 +822,11 @@ it("updates the project oauth configuration", async ({ expect }) => {
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "New Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_dark_mode_url": null,
|
||||
"logo_full_dark_mode_url": null,
|
||||
"logo_full_url": null,
|
||||
"logo_url": null,
|
||||
"owner_team_id": "<stripped UUID>",
|
||||
},
|
||||
@ -855,9 +877,11 @@ it("updates the project oauth configuration", async ({ expect }) => {
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "New Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_dark_mode_url": null,
|
||||
"logo_full_dark_mode_url": null,
|
||||
"logo_full_url": null,
|
||||
"logo_url": null,
|
||||
"owner_team_id": "<stripped UUID>",
|
||||
},
|
||||
@ -912,9 +936,11 @@ it("updates the project oauth configuration", async ({ expect }) => {
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "New Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_dark_mode_url": null,
|
||||
"logo_full_dark_mode_url": null,
|
||||
"logo_full_url": null,
|
||||
"logo_url": null,
|
||||
"owner_team_id": "<stripped UUID>",
|
||||
},
|
||||
@ -964,9 +990,11 @@ it("updates the project oauth configuration", async ({ expect }) => {
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "New Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_dark_mode_url": null,
|
||||
"logo_full_dark_mode_url": null,
|
||||
"logo_full_url": null,
|
||||
"logo_url": null,
|
||||
"owner_team_id": "<stripped UUID>",
|
||||
},
|
||||
@ -1031,9 +1059,11 @@ it("updates the project oauth configuration", async ({ expect }) => {
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "New Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_dark_mode_url": null,
|
||||
"logo_full_dark_mode_url": null,
|
||||
"logo_full_url": null,
|
||||
"logo_url": null,
|
||||
"owner_team_id": "<stripped UUID>",
|
||||
},
|
||||
@ -1098,9 +1128,11 @@ it("updates the project oauth configuration", async ({ expect }) => {
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "New Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_dark_mode_url": null,
|
||||
"logo_full_dark_mode_url": null,
|
||||
"logo_full_url": null,
|
||||
"logo_url": null,
|
||||
"owner_team_id": "<stripped UUID>",
|
||||
},
|
||||
|
||||
@ -255,9 +255,11 @@ it("can customize default team permissions", async ({ expect }) => {
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "New Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_dark_mode_url": null,
|
||||
"logo_full_dark_mode_url": null,
|
||||
"logo_full_url": null,
|
||||
"logo_url": null,
|
||||
"owner_team_id": "<stripped UUID>",
|
||||
},
|
||||
|
||||
@ -43,15 +43,16 @@ export const emptyEmailTheme = deindent`
|
||||
`;
|
||||
|
||||
export const LightEmailTheme = `import { Html, Head, Tailwind, Body, Container, Link } from '@react-email/components';
|
||||
import { ThemeProps } from "@stackframe/emails"
|
||||
import { ThemeProps, ProjectLogo } from "@stackframe/emails";
|
||||
|
||||
export function EmailTheme({ children, unsubscribeLink }: ThemeProps) {
|
||||
export function EmailTheme({ children, unsubscribeLink, projectLogos }: ThemeProps) {
|
||||
return (
|
||||
<Html>
|
||||
<Head />
|
||||
<Tailwind>
|
||||
<Body className="bg-[#fafbfb] font-sans text-base">
|
||||
<Container className="bg-white p-[45px] rounded-lg">
|
||||
<ProjectLogo data={projectLogos} mode="light" />
|
||||
{children}
|
||||
</Container>
|
||||
{unsubscribeLink && (
|
||||
@ -67,26 +68,27 @@ export function EmailTheme({ children, unsubscribeLink }: ThemeProps) {
|
||||
}
|
||||
|
||||
EmailTheme.PreviewProps = {
|
||||
unsubscribeLink: "https://example.com"
|
||||
unsubscribeLink: "https://example.com",
|
||||
} satisfies Partial<ThemeProps>
|
||||
`;
|
||||
|
||||
|
||||
const DarkEmailTheme = `import { Html, Head, Tailwind, Body, Container, Link } from '@react-email/components';
|
||||
import { ThemeProps } from "@stackframe/emails"
|
||||
import { ThemeProps, ProjectLogo } from "@stackframe/emails";
|
||||
|
||||
export function EmailTheme({ children, unsubscribeLink }: ThemeProps) {
|
||||
export function EmailTheme({ children, unsubscribeLink, projectLogos }: ThemeProps) {
|
||||
return (
|
||||
<Html>
|
||||
<Head />
|
||||
<Tailwind>
|
||||
<Body className="bg-[#323232] font-sans text-white">
|
||||
<Container className="bg-black p-[45px] rounded-lg">
|
||||
<ProjectLogo data={projectLogos} mode="dark" />
|
||||
{children}
|
||||
</Container>
|
||||
{unsubscribeLink && (
|
||||
<div className="p-4">
|
||||
<Link href={unsubscribeLink}>Click here{" "}</Link>
|
||||
<Link href={unsubscribeLink} className="text-gray-300">Click here{" "}</Link>
|
||||
to unsubscribe from these emails
|
||||
</div>
|
||||
)}
|
||||
@ -97,7 +99,7 @@ export function EmailTheme({ children, unsubscribeLink }: ThemeProps) {
|
||||
}
|
||||
|
||||
EmailTheme.PreviewProps = {
|
||||
unsubscribeLink: "https://example.com"
|
||||
unsubscribeLink: "https://example.com",
|
||||
} satisfies Partial<ThemeProps>
|
||||
`;
|
||||
|
||||
|
||||
@ -66,7 +66,9 @@ export const projectsCrudAdminReadSchema = yupObject({
|
||||
display_name: schemaFields.projectDisplayNameSchema.defined(),
|
||||
description: schemaFields.projectDescriptionSchema.nonNullable().defined(),
|
||||
logo_url: schemaFields.projectLogoUrlSchema.nullable().optional(),
|
||||
full_logo_url: schemaFields.projectFullLogoUrlSchema.nullable().optional(),
|
||||
logo_full_url: schemaFields.projectLogoFullUrlSchema.nullable().optional(),
|
||||
logo_dark_mode_url: schemaFields.projectLogoDarkModeUrlSchema.nullable().optional(),
|
||||
logo_full_dark_mode_url: schemaFields.projectLogoFullDarkModeUrlSchema.nullable().optional(),
|
||||
created_at_millis: schemaFields.projectCreatedAtMillisSchema.defined(),
|
||||
is_production_mode: schemaFields.projectIsProductionModeSchema.defined(),
|
||||
owner_team_id: schemaFields.yupString().nullable().defined(),
|
||||
@ -117,7 +119,9 @@ export const projectsCrudAdminUpdateSchema = yupObject({
|
||||
display_name: schemaFields.projectDisplayNameSchema.optional(),
|
||||
description: schemaFields.projectDescriptionSchema.optional(),
|
||||
logo_url: schemaFields.projectLogoUrlSchema.nullable().optional(),
|
||||
full_logo_url: schemaFields.projectFullLogoUrlSchema.nullable().optional(),
|
||||
logo_full_url: schemaFields.projectLogoFullUrlSchema.nullable().optional(),
|
||||
logo_dark_mode_url: schemaFields.projectLogoDarkModeUrlSchema.nullable().optional(),
|
||||
logo_full_dark_mode_url: schemaFields.projectLogoFullDarkModeUrlSchema.nullable().optional(),
|
||||
is_production_mode: schemaFields.projectIsProductionModeSchema.optional(),
|
||||
config: yupObject({
|
||||
sign_up_enabled: schemaFields.projectSignUpEnabledSchema.optional(),
|
||||
|
||||
@ -503,7 +503,9 @@ export const projectIdSchema = yupString().test((v) => v === undefined || v ===
|
||||
export const projectBranchIdSchema = yupString().nonEmpty().max(255).meta({ openapiField: { description: _idDescription('project branch'), exampleValue: 'main' } });
|
||||
export const projectDisplayNameSchema = yupString().meta({ openapiField: { description: _displayNameDescription('project'), exampleValue: 'MyMusic' } });
|
||||
export const projectLogoUrlSchema = urlSchema.max(MAX_IMAGE_SIZE_BASE64_BYTES).meta({ openapiField: { description: 'URL of the logo for the project. This is usually a close to 1:1 image of the company logo.', exampleValue: 'https://example.com/logo.png' } });
|
||||
export const projectFullLogoUrlSchema = urlSchema.max(MAX_IMAGE_SIZE_BASE64_BYTES).meta({ openapiField: { description: 'URL of the full logo for the project. This is usually a vertical image with the logo and the company name.', exampleValue: 'https://example.com/full-logo.png' } });
|
||||
export const projectLogoFullUrlSchema = urlSchema.max(MAX_IMAGE_SIZE_BASE64_BYTES).meta({ openapiField: { description: 'URL of the full logo for the project. This is usually a vertical image with the logo and the company name.', exampleValue: 'https://example.com/full-logo.png' } });
|
||||
export const projectLogoDarkModeUrlSchema = urlSchema.max(MAX_IMAGE_SIZE_BASE64_BYTES).meta({ openapiField: { description: 'URL of the dark mode logo for the project. This is usually a close to 1:1 image of the company logo optimized for dark backgrounds.', exampleValue: 'https://example.com/logo-dark.png' } });
|
||||
export const projectLogoFullDarkModeUrlSchema = urlSchema.max(MAX_IMAGE_SIZE_BASE64_BYTES).meta({ openapiField: { description: 'URL of the dark mode full logo for the project. This is usually a vertical image with the logo and the company name optimized for dark backgrounds.', exampleValue: 'https://example.com/full-logo-dark.png' } });
|
||||
export const projectDescriptionSchema = yupString().nullable().meta({ openapiField: { description: 'A human readable description of the project', exampleValue: 'A music streaming service' } });
|
||||
export const projectCreatedAtMillisSchema = yupNumber().meta({ openapiField: { description: _createdAtMillisDescription('project'), exampleValue: 1630000000000 } });
|
||||
export const projectIsProductionModeSchema = yupBoolean().meta({ openapiField: { description: 'Whether the project is in production mode', exampleValue: true } });
|
||||
|
||||
@ -127,7 +127,9 @@ export class _StackAdminAppImplIncomplete<HasTokenStore extends boolean, Project
|
||||
isProductionMode: data.is_production_mode,
|
||||
ownerTeamId: data.owner_team_id,
|
||||
logoUrl: data.logo_url,
|
||||
fullLogoUrl: data.full_logo_url,
|
||||
logoFullUrl: data.logo_full_url,
|
||||
logoDarkModeUrl: data.logo_dark_mode_url,
|
||||
logoFullDarkModeUrl: data.logo_full_dark_mode_url,
|
||||
config: {
|
||||
signUpEnabled: data.config.sign_up_enabled,
|
||||
credentialEnabled: data.config.credential_enabled,
|
||||
|
||||
@ -21,7 +21,9 @@ export type AdminProject = {
|
||||
readonly isProductionMode: boolean,
|
||||
readonly ownerTeamId: string | null,
|
||||
readonly logoUrl: string | null | undefined,
|
||||
readonly fullLogoUrl: string | null | undefined,
|
||||
readonly logoFullUrl: string | null | undefined,
|
||||
readonly logoDarkModeUrl: string | null | undefined,
|
||||
readonly logoFullDarkModeUrl: string | null | undefined,
|
||||
|
||||
readonly config: AdminProjectConfig,
|
||||
|
||||
@ -47,7 +49,9 @@ export type AdminProjectUpdateOptions = {
|
||||
description?: string,
|
||||
isProductionMode?: boolean,
|
||||
logoUrl?: string | null,
|
||||
fullLogoUrl?: string | null,
|
||||
logoFullUrl?: string | null,
|
||||
logoDarkModeUrl?: string | null,
|
||||
logoFullDarkModeUrl?: string | null,
|
||||
config?: AdminProjectConfigUpdateOptions,
|
||||
};
|
||||
export function adminProjectUpdateOptionsToCrud(options: AdminProjectUpdateOptions): ProjectsCrud["Admin"]["Update"] {
|
||||
@ -56,7 +60,9 @@ export function adminProjectUpdateOptionsToCrud(options: AdminProjectUpdateOptio
|
||||
description: options.description,
|
||||
is_production_mode: options.isProductionMode,
|
||||
logo_url: options.logoUrl,
|
||||
full_logo_url: options.fullLogoUrl,
|
||||
logo_full_url: options.logoFullUrl,
|
||||
logo_dark_mode_url: options.logoDarkModeUrl,
|
||||
logo_full_dark_mode_url: options.logoFullDarkModeUrl,
|
||||
config: {
|
||||
domains: options.config?.domains?.map((d) => ({
|
||||
domain: d.domain,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user