Merge branch 'feat/refresh' into feat-early-adopters

This commit is contained in:
Timur Ercan
2023-09-28 13:16:09 +02:00
committed by GitHub
18 changed files with 166 additions and 128 deletions

View File

@ -177,9 +177,7 @@ export const createSinglePlayerDocument = async (
}, },
); );
// Todo: Handle `downloadLink`
const template = createElement(DocumentSelfSignedEmailTemplate, { const template = createElement(DocumentSelfSignedEmailTemplate, {
downloadLink: `${process.env.NEXT_PUBLIC_MARKETING_URL}/single-player-mode/${documentToken}`,
documentName: documentName, documentName: documentName,
assetBaseUrl: process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000', assetBaseUrl: process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000',
}); });
@ -197,6 +195,7 @@ export const createSinglePlayerDocument = async (
subject: 'Document signed', subject: 'Document signed',
html: render(template), html: render(template),
text: render(template, { plainText: true }), text: render(template, { plainText: true }),
attachments: [{ content: Buffer.from(pdfBytes), filename: documentName }],
}); });
return documentToken; return documentToken;

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 B

View File

@ -1,7 +1,9 @@
import { Button, Column, Img, Row, Section, Tailwind, Text } from '@react-email/components'; import { Button, Column, Img, Section, Tailwind, Text } from '@react-email/components';
import * as config from '@documenso/tailwind-config'; import * as config from '@documenso/tailwind-config';
import { TemplateDocumentImage } from './template-document-image';
export interface TemplateDocumentCompletedProps { export interface TemplateDocumentCompletedProps {
downloadLink: string; downloadLink: string;
documentName: string; documentName: string;
@ -27,27 +29,20 @@ export const TemplateDocumentCompleted = ({
}, },
}} }}
> >
<Section> <TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
<Row className="table-fixed">
<Column />
<Column> <Section>
<Section className="mb-4">
<Column align="center">
<Text className="text-base font-semibold text-[#7AC455]">
<Img <Img
className="h-42 mx-auto" src={getAssetUrl('/static/completed.png')}
src={getAssetUrl('/static/document.png')} className="-mt-0.5 mr-2 inline h-7 w-7 align-middle"
alt="Documenso"
/> />
</Column>
<Column />
</Row>
</Section>
<Section>
<Text className="mb-4 flex items-center justify-center text-center text-base font-semibold text-[#7AC455]">
<Img src={getAssetUrl('/static/completed.png')} className="-mb-0.5 mr-2 inline h-7 w-7" />
Completed Completed
</Text> </Text>
</Column>
</Section>
<Text className="text-primary mb-0 text-center text-lg font-semibold"> <Text className="text-primary mb-0 text-center text-lg font-semibold">
{documentName} was signed by all signers {documentName} was signed by all signers
@ -66,10 +61,13 @@ export const TemplateDocumentCompleted = ({
Review Review
</Button> */} </Button> */}
<Button <Button
className="inline-flex items-center justify-center rounded-lg border border-solid border-slate-200 px-4 py-2 text-center text-sm font-medium text-black no-underline" className="rounded-lg border border-solid border-slate-200 px-4 py-2 text-center text-sm font-medium text-black no-underline"
href={downloadLink} href={downloadLink}
> >
<Img src={getAssetUrl('/static/download.png')} className="-mb-1 mr-2 inline h-5 w-5" /> <Img
src={getAssetUrl('/static/download.png')}
className="mb-0.5 mr-2 inline h-5 w-5 align-middle"
/>
Download Download
</Button> </Button>
</Section> </Section>

View File

@ -0,0 +1,28 @@
import { Column, Img, Row, Section } from '@react-email/components';
export interface TemplateDocumentImageProps {
assetBaseUrl: string;
className?: string;
}
export const TemplateDocumentImage = ({ assetBaseUrl, className }: TemplateDocumentImageProps) => {
const getAssetUrl = (path: string) => {
return new URL(path, assetBaseUrl).toString();
};
return (
<Section className={className}>
<Row className="table-fixed">
<Column />
<Column>
<Img className="h-42 mx-auto" src={getAssetUrl('/static/document.png')} alt="Documenso" />
</Column>
<Column />
</Row>
</Section>
);
};
export default TemplateDocumentImage;

View File

@ -1,7 +1,9 @@
import { Button, Column, Img, Row, Section, Tailwind, Text } from '@react-email/components'; import { Button, Section, Tailwind, Text } from '@react-email/components';
import * as config from '@documenso/tailwind-config'; import * as config from '@documenso/tailwind-config';
import { TemplateDocumentImage } from './template-document-image';
export interface TemplateDocumentInviteProps { export interface TemplateDocumentInviteProps {
inviterName: string; inviterName: string;
inviterEmail: string; inviterEmail: string;
@ -16,10 +18,6 @@ export const TemplateDocumentInvite = ({
signDocumentLink, signDocumentLink,
assetBaseUrl, assetBaseUrl,
}: TemplateDocumentInviteProps) => { }: TemplateDocumentInviteProps) => {
const getAssetUrl = (path: string) => {
return new URL(path, assetBaseUrl).toString();
};
return ( return (
<Tailwind <Tailwind
config={{ config={{
@ -30,21 +28,7 @@ export const TemplateDocumentInvite = ({
}, },
}} }}
> >
<Section className="mt-4"> <TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
<Row className="table-fixed">
<Column />
<Column>
<Img
className="h-42 mx-auto"
src={getAssetUrl('/static/document.png')}
alt="Documenso"
/>
</Column>
<Column />
</Row>
</Section>
<Section> <Section>
<Text className="text-primary mx-auto mb-0 max-w-[80%] text-center text-lg font-semibold"> <Text className="text-primary mx-auto mb-0 max-w-[80%] text-center text-lg font-semibold">

View File

@ -1,7 +1,9 @@
import { Column, Img, Row, Section, Tailwind, Text } from '@react-email/components'; import { Column, Img, Section, Tailwind, Text } from '@react-email/components';
import * as config from '@documenso/tailwind-config'; import * as config from '@documenso/tailwind-config';
import { TemplateDocumentImage } from './template-document-image';
export interface TemplateDocumentPendingProps { export interface TemplateDocumentPendingProps {
documentName: string; documentName: string;
assetBaseUrl: string; assetBaseUrl: string;
@ -25,27 +27,20 @@ export const TemplateDocumentPending = ({
}, },
}} }}
> >
<Section> <TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
<Row className="table-fixed">
<Column />
<Column> <Section>
<Section className="mb-4">
<Column align="center">
<Text className="text-base font-semibold text-blue-500">
<Img <Img
className="h-42 mx-auto" src={getAssetUrl('/static/clock.png')}
src={getAssetUrl('/static/document.png')} className="-mt-0.5 mr-2 inline h-7 w-7 align-middle"
alt="Documenso"
/> />
</Column>
<Column />
</Row>
</Section>
<Section>
<Text className="mb-4 flex items-center justify-center text-center text-base font-semibold text-blue-500">
<Img src={getAssetUrl('/static/clock.png')} className="-mb-0.5 mr-2 inline h-7 w-7" />
Waiting for others Waiting for others
</Text> </Text>
</Column>
</Section>
<Text className="text-primary mb-0 text-center text-lg font-semibold"> <Text className="text-primary mb-0 text-center text-lg font-semibold">
{documentName} has been signed {documentName} has been signed

View File

@ -1,18 +1,20 @@
import { Button, Img, Section, Tailwind, Text } from '@react-email/components'; import { Button, Column, Img, Link, Section, Tailwind, Text } from '@react-email/components';
import * as config from '@documenso/tailwind-config'; import * as config from '@documenso/tailwind-config';
import { TemplateDocumentImage } from './template-document-image';
export interface TemplateDocumentSelfSignedProps { export interface TemplateDocumentSelfSignedProps {
downloadLink: string;
documentName: string; documentName: string;
assetBaseUrl: string; assetBaseUrl: string;
} }
export const TemplateDocumentSelfSigned = ({ export const TemplateDocumentSelfSigned = ({
downloadLink,
documentName, documentName,
assetBaseUrl, assetBaseUrl,
}: TemplateDocumentSelfSignedProps) => { }: TemplateDocumentSelfSignedProps) => {
const signUpUrl = `${process.env.NEXT_PUBLIC_WEBAPP_URL ?? 'http://localhost:3000'}/signup`;
const getAssetUrl = (path: string) => { const getAssetUrl = (path: string) => {
return new URL(path, assetBaseUrl).toString(); return new URL(path, assetBaseUrl).toString();
}; };
@ -27,39 +29,59 @@ export const TemplateDocumentSelfSigned = ({
}, },
}} }}
> >
<Section className="flex-row items-center justify-center"> <TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
<div className="flex items-center justify-center p-4">
<Img className="h-42" src={getAssetUrl('/static/document.png')} alt="Documenso" />
</div>
<Text className="mb-4 flex items-center justify-center text-center text-base font-semibold text-[#7AC455]"> <Section className="flex-row items-center justify-center">
<Img src={getAssetUrl('/static/completed.png')} className="-mb-0.5 mr-2 inline h-7 w-7" /> <Section>
<Column align="center">
<Text className="text-base font-semibold text-[#7AC455]">
<Img
src={getAssetUrl('/static/completed.png')}
className="-mt-0.5 mr-2 inline h-7 w-7 align-middle"
/>
Completed Completed
</Text> </Text>
</Column>
</Section>
<Text className="text-primary mb-0 text-center text-lg font-semibold"> <Text className="text-primary mb-0 mt-6 text-center text-lg font-semibold">
You have signed {documentName} You have signed {documentName}
</Text> </Text>
<Text className="my-1 text-center text-base text-slate-400"> <Text className="mx-auto mb-6 mt-1 max-w-[80%] text-center text-base text-slate-400">
Check out our plans to access the full suite of features. Create a{' '}
<Link
href={signUpUrl}
target="_blank"
className="text-documenso-700 hover:text-documenso-600 whitespace-nowrap"
>
free account
</Link>{' '}
to access your signed documents at any time.
</Text> </Text>
<Section className="mb-6 mt-8 text-center"> <Section className="mb-6 mt-8 text-center">
<Button <Button
className="mr-4 inline-flex items-center justify-center rounded-lg border border-solid border-slate-200 px-4 py-2 text-center text-sm font-medium text-black no-underline" href={signUpUrl}
className="mr-4 rounded-lg border border-solid border-slate-200 px-4 py-2 text-center text-sm font-medium text-black no-underline"
>
<Img
src={getAssetUrl('/static/user-plus.png')}
className="mb-0.5 mr-2 inline h-5 w-5 align-middle"
/>
Create account
</Button>
<Button
className="rounded-lg border border-solid border-slate-200 px-4 py-2 text-center text-sm font-medium text-black no-underline"
href="https://documenso.com/pricing" href="https://documenso.com/pricing"
> >
<Img src={getAssetUrl('/static/review.png')} className="-mb-1 mr-2 inline h-5 w-5" /> <Img
src={getAssetUrl('/static/review.png')}
className="mb-0.5 mr-2 inline h-5 w-5 align-middle"
/>
View plans View plans
</Button> </Button>
<Button
className="inline-flex items-center justify-center rounded-lg border border-solid border-slate-200 px-4 py-2 text-center text-sm font-medium text-black no-underline"
href={downloadLink}
>
<Img src={getAssetUrl('/static/download.png')} className="-mb-1 mr-2 inline h-5 w-5" />
Download
</Button>
</Section> </Section>
</Section> </Section>
</Tailwind> </Tailwind>

View File

@ -1,7 +1,9 @@
import { Button, Img, Section, Tailwind, Text } from '@react-email/components'; import { Button, Section, Tailwind, Text } from '@react-email/components';
import * as config from '@documenso/tailwind-config'; import * as config from '@documenso/tailwind-config';
import { TemplateDocumentImage } from './template-document-image';
export type TemplateForgotPasswordProps = { export type TemplateForgotPasswordProps = {
resetPasswordLink: string; resetPasswordLink: string;
assetBaseUrl: string; assetBaseUrl: string;
@ -11,10 +13,6 @@ export const TemplateForgotPassword = ({
resetPasswordLink, resetPasswordLink,
assetBaseUrl, assetBaseUrl,
}: TemplateForgotPasswordProps) => { }: TemplateForgotPasswordProps) => {
const getAssetUrl = (path: string) => {
return new URL(path, assetBaseUrl).toString();
};
return ( return (
<Tailwind <Tailwind
config={{ config={{
@ -25,11 +23,9 @@ export const TemplateForgotPassword = ({
}, },
}} }}
> >
<Section className="mt-4 flex-row items-center justify-center"> <TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
<div className="flex items-center justify-center p-4">
<Img className="h-42" src={getAssetUrl('/static/document.png')} alt="Documenso" />
</div>
<Section className="flex-row items-center justify-center">
<Text className="text-primary mx-auto mb-0 max-w-[80%] text-center text-lg font-semibold"> <Text className="text-primary mx-auto mb-0 max-w-[80%] text-center text-lg font-semibold">
Forgot your password? Forgot your password?
</Text> </Text>

View File

@ -1,7 +1,9 @@
import { Img, Section, Tailwind, Text } from '@react-email/components'; import { Button, Section, Tailwind, Text } from '@react-email/components';
import * as config from '@documenso/tailwind-config'; import * as config from '@documenso/tailwind-config';
import { TemplateDocumentImage } from './template-document-image';
export interface TemplateResetPasswordProps { export interface TemplateResetPasswordProps {
userName: string; userName: string;
userEmail: string; userEmail: string;
@ -9,10 +11,6 @@ export interface TemplateResetPasswordProps {
} }
export const TemplateResetPassword = ({ assetBaseUrl }: TemplateResetPasswordProps) => { export const TemplateResetPassword = ({ assetBaseUrl }: TemplateResetPasswordProps) => {
const getAssetUrl = (path: string) => {
return new URL(path, assetBaseUrl).toString();
};
return ( return (
<Tailwind <Tailwind
config={{ config={{
@ -23,11 +21,9 @@ export const TemplateResetPassword = ({ assetBaseUrl }: TemplateResetPasswordPro
}, },
}} }}
> >
<Section className="mt-4 flex-row items-center justify-center"> <TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
<div className="flex items-center justify-center p-4">
<Img className="h-42" src={getAssetUrl('/static/document.png')} alt="Documenso" />
</div>
<Section className="flex-row items-center justify-center">
<Text className="text-primary mx-auto mb-0 max-w-[80%] text-center text-lg font-semibold"> <Text className="text-primary mx-auto mb-0 max-w-[80%] text-center text-lg font-semibold">
Password updated! Password updated!
</Text> </Text>
@ -35,6 +31,15 @@ export const TemplateResetPassword = ({ assetBaseUrl }: TemplateResetPasswordPro
<Text className="my-1 text-center text-base text-slate-400"> <Text className="my-1 text-center text-base text-slate-400">
Your password has been updated. Your password has been updated.
</Text> </Text>
<Section className="mb-6 mt-8 text-center">
<Button
className="bg-documenso-500 inline-flex items-center justify-center rounded-lg px-6 py-3 text-center text-sm font-medium text-black no-underline"
href={`${process.env.NEXT_PUBLIC_WEBAPP_URL ?? 'http://localhost:3000'}/signin`}
>
Sign In
</Button>
</Section>
</Section> </Section>
</Tailwind> </Tailwind>
); );

View File

@ -15,7 +15,7 @@ import {
TemplateDocumentCompleted, TemplateDocumentCompleted,
TemplateDocumentCompletedProps, TemplateDocumentCompletedProps,
} from '../template-components/template-document-completed'; } from '../template-components/template-document-completed';
import TemplateFooter from '../template-components/template-footer'; import { TemplateFooter } from '../template-components/template-footer';
export type DocumentCompletedEmailTemplateProps = Partial<TemplateDocumentCompletedProps>; export type DocumentCompletedEmailTemplateProps = Partial<TemplateDocumentCompletedProps>;

View File

@ -18,7 +18,7 @@ import {
TemplateDocumentInvite, TemplateDocumentInvite,
TemplateDocumentInviteProps, TemplateDocumentInviteProps,
} from '../template-components/template-document-invite'; } from '../template-components/template-document-invite';
import TemplateFooter from '../template-components/template-footer'; import { TemplateFooter } from '../template-components/template-footer';
export type DocumentInviteEmailTemplateProps = Partial<TemplateDocumentInviteProps> & { export type DocumentInviteEmailTemplateProps = Partial<TemplateDocumentInviteProps> & {
customBody?: string; customBody?: string;

View File

@ -15,12 +15,11 @@ import {
TemplateDocumentSelfSigned, TemplateDocumentSelfSigned,
TemplateDocumentSelfSignedProps, TemplateDocumentSelfSignedProps,
} from '../template-components/template-document-self-signed'; } from '../template-components/template-document-self-signed';
import TemplateFooter from '../template-components/template-footer'; import { TemplateFooter } from '../template-components/template-footer';
export type DocumentSelfSignedTemplateProps = TemplateDocumentSelfSignedProps; export type DocumentSelfSignedTemplateProps = TemplateDocumentSelfSignedProps;
export const DocumentSelfSignedEmailTemplate = ({ export const DocumentSelfSignedEmailTemplate = ({
downloadLink = 'https://documenso.com',
documentName = 'Open Source Pledge.pdf', documentName = 'Open Source Pledge.pdf',
assetBaseUrl = 'http://localhost:3002', assetBaseUrl = 'http://localhost:3002',
}: DocumentSelfSignedTemplateProps) => { }: DocumentSelfSignedTemplateProps) => {
@ -54,7 +53,6 @@ export const DocumentSelfSignedEmailTemplate = ({
/> />
<TemplateDocumentSelfSigned <TemplateDocumentSelfSigned
downloadLink={downloadLink}
documentName={documentName} documentName={documentName}
assetBaseUrl={assetBaseUrl} assetBaseUrl={assetBaseUrl}
/> />

View File

@ -11,7 +11,7 @@ import {
import config from '@documenso/tailwind-config'; import config from '@documenso/tailwind-config';
import TemplateFooter from '../template-components/template-footer'; import { TemplateFooter } from '../template-components/template-footer';
import { import {
TemplateForgotPassword, TemplateForgotPassword,
TemplateForgotPasswordProps, TemplateForgotPasswordProps,

View File

@ -14,7 +14,7 @@ import {
import config from '@documenso/tailwind-config'; import config from '@documenso/tailwind-config';
import TemplateFooter from '../template-components/template-footer'; import { TemplateFooter } from '../template-components/template-footer';
import { import {
TemplateResetPassword, TemplateResetPassword,
TemplateResetPasswordProps, TemplateResetPasswordProps,

View File

@ -126,12 +126,12 @@ export const AddSignersFormPartial = ({
<AnimatePresence> <AnimatePresence>
{signers.map((signer, index) => ( {signers.map((signer, index) => (
<motion.div <motion.div
key={signer.formId} key={signer.id}
data-native-id={signer.nativeId} data-native-id={signer.nativeId}
className="flex flex-wrap items-end gap-x-4" className="flex flex-wrap items-end gap-x-4"
> >
<div className="flex-1"> <div className="flex-1">
<Label htmlFor={`signer-${signer.formId}-email`}> <Label htmlFor={`signer-${signer.id}-email`}>
Email Email
<span className="text-destructive ml-1 inline-block font-medium">*</span> <span className="text-destructive ml-1 inline-block font-medium">*</span>
</Label> </Label>
@ -141,7 +141,7 @@ export const AddSignersFormPartial = ({
name={`signers.${index}.email`} name={`signers.${index}.email`}
render={({ field }) => ( render={({ field }) => (
<Input <Input
id={`signer-${signer.formId}-email`} id={`signer-${signer.id}-email`}
type="email" type="email"
className="bg-background mt-2" className="bg-background mt-2"
disabled={isSubmitting || hasBeenSentToRecipientId(signer.nativeId)} disabled={isSubmitting || hasBeenSentToRecipientId(signer.nativeId)}
@ -153,14 +153,14 @@ export const AddSignersFormPartial = ({
</div> </div>
<div className="flex-1"> <div className="flex-1">
<Label htmlFor={`signer-${signer.formId}-name`}>Name</Label> <Label htmlFor={`signer-${signer.id}-name`}>Name</Label>
<Controller <Controller
control={control} control={control}
name={`signers.${index}.name`} name={`signers.${index}.name`}
render={({ field }) => ( render={({ field }) => (
<Input <Input
id={`signer-${signer.formId}-name`} id={`signer-${signer.id}-name`}
type="text" type="text"
className="bg-background mt-2" className="bg-background mt-2"
disabled={isSubmitting || hasBeenSentToRecipientId(signer.nativeId)} disabled={isSubmitting || hasBeenSentToRecipientId(signer.nativeId)}
@ -195,7 +195,11 @@ export const AddSignersFormPartial = ({
</AnimatePresence> </AnimatePresence>
</div> </div>
<FormErrorMessage className="mt-2" error={errors.signers} /> <FormErrorMessage
className="mt-2"
// Dirty hack to handle errors when .root is populated for an array type
error={'signers__root' in errors && errors['signers__root']}
/>
<div className="mt-4"> <div className="mt-4">
<Button type="button" disabled={isSubmitting} onClick={() => onAddSigner()}> <Button type="button" disabled={isSubmitting} onClick={() => onAddSigner()}>

View File

@ -1,19 +1,24 @@
import { z } from 'zod'; import { z } from 'zod';
export const ZAddSignersFormSchema = z.object({ export const ZAddSignersFormSchema = z
signers: z .object({
.array( signers: z.array(
z.object({ z.object({
formId: z.string().min(1), formId: z.string().min(1),
nativeId: z.number().optional(), nativeId: z.number().optional(),
email: z.string().min(1).email(), email: z.string().min(1).email(),
name: z.string(), name: z.string(),
}), }),
) ),
.refine((signers) => { })
const emails = signers.map((signer) => signer.email); .refine(
(schema) => {
const emails = schema.signers.map((signer) => signer.email.toLowerCase());
return new Set(emails).size === emails.length; return new Set(emails).size === emails.length;
}, 'Signers must have unique emails'), },
}); // Dirty hack to handle errors when .root is populated for an array type
{ message: 'Signers must have unique emails', path: ['signers__root'] },
);
export type TAddSignersFormSchema = z.infer<typeof ZAddSignersFormSchema>; export type TAddSignersFormSchema = z.infer<typeof ZAddSignersFormSchema>;

View File

@ -4,13 +4,17 @@ import { cn } from '@documenso/ui/lib/utils';
export type FormErrorMessageProps = { export type FormErrorMessageProps = {
className?: string; className?: string;
error: { message?: string } | undefined; error: { message?: string } | undefined | unknown;
};
const isErrorWithMessage = (error: unknown): error is { message?: string } => {
return typeof error === 'object' && error !== null && 'message' in error;
}; };
export const FormErrorMessage = ({ error, className }: FormErrorMessageProps) => { export const FormErrorMessage = ({ error, className }: FormErrorMessageProps) => {
return ( return (
<AnimatePresence> <AnimatePresence>
{error && ( {isErrorWithMessage(error) && (
<motion.p <motion.p
initial={{ initial={{
opacity: 0, opacity: 0,