Merge branch 'main' into feat/public-profiles

This commit is contained in:
Lucas Smith
2024-06-13 15:20:26 +10:00
committed by GitHub
26 changed files with 608 additions and 584 deletions

View File

@ -6,5 +6,5 @@ import { getPricesByPlan } from './get-prices-by-plan';
* Returns the Stripe prices of items that affect the amount of documents a user can create.
*/
export const getDocumentRelatedPrices = async () => {
return await getPricesByPlan([STRIPE_PLAN_TYPE.COMMUNITY, STRIPE_PLAN_TYPE.ENTERPRISE]);
return await getPricesByPlan([STRIPE_PLAN_TYPE.REGULAR, STRIPE_PLAN_TYPE.COMMUNITY, STRIPE_PLAN_TYPE.ENTERPRISE]);
};

View File

@ -1,6 +1,7 @@
import type Stripe from 'stripe';
import { stripe } from '@documenso/lib/server-only/stripe';
import type { STRIPE_PLAN_TYPE } from '@documenso/lib/constants/billing';
// Utility type to handle usage of the `expand` option.
type PriceWithProduct = Stripe.Price & { product: Stripe.Product };
@ -11,7 +12,7 @@ export type GetPricesByIntervalOptions = {
/**
* Filter products by their meta 'plan' attribute.
*/
plan?: 'community';
plan?: STRIPE_PLAN_TYPE.COMMUNITY | STRIPE_PLAN_TYPE.REGULAR;
};
export const getPricesByInterval = async ({ plan }: GetPricesByIntervalOptions = {}) => {

View File

@ -6,5 +6,5 @@ import { getPricesByPlan } from './get-prices-by-plan';
* Returns the prices of items that count as the account's primary plan.
*/
export const getPrimaryAccountPlanPrices = async () => {
return await getPricesByPlan([STRIPE_PLAN_TYPE.COMMUNITY, STRIPE_PLAN_TYPE.ENTERPRISE]);
return await getPricesByPlan([STRIPE_PLAN_TYPE.REGULAR, STRIPE_PLAN_TYPE.COMMUNITY, STRIPE_PLAN_TYPE.ENTERPRISE]);
};

View File

@ -1,3 +1,4 @@
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
import config from '@documenso/tailwind-config';
import {
@ -14,9 +15,11 @@ import {
} from '../components';
import TemplateDocumentImage from '../template-components/template-document-image';
import { TemplateFooter } from '../template-components/template-footer';
import { RecipientRole } from '.prisma/client';
export type DocumentCompletedEmailTemplateProps = {
recipientName?: string;
recipientRole?: RecipientRole;
documentLink?: string;
documentName?: string;
assetBaseUrl?: string;
@ -24,11 +27,14 @@ export type DocumentCompletedEmailTemplateProps = {
export const DocumentCreatedFromDirectTemplateEmailTemplate = ({
recipientName = 'John Doe',
recipientRole = RecipientRole.SIGNER,
documentLink = 'http://localhost:3000',
documentName = 'Open Source Pledge.pdf',
assetBaseUrl = 'http://localhost:3002',
}: DocumentCompletedEmailTemplateProps) => {
const previewText = `Completed Document`;
const action = RECIPIENT_ROLES_DESCRIPTION[recipientRole].actioned.toLowerCase();
const previewText = `Document created from direct template`;
const getAssetUrl = (path: string) => {
return new URL(path, assetBaseUrl).toString();
@ -61,7 +67,7 @@ export const DocumentCreatedFromDirectTemplateEmailTemplate = ({
<Section>
<Text className="text-primary mb-0 text-center text-lg font-semibold">
{recipientName} signed a document by using one of your direct links
{recipientName} {action} a document by using one of your direct links
</Text>
<div className="mx-auto my-2 w-fit rounded-lg bg-gray-50 px-4 py-2 text-sm text-slate-600">

View File

@ -18,7 +18,7 @@ export const downloadPDF = async ({ documentData, fileName }: DownloadPDFProps)
const baseTitle = (fileName ?? 'document').replace(/\.pdf$/, '');
downloadFile({
filename: `${baseTitle}.pdf`,
filename: `${baseTitle}_signed.pdf`,
data: blob,
});
};

View File

@ -4,6 +4,7 @@ export enum STRIPE_CUSTOMER_TYPE {
}
export enum STRIPE_PLAN_TYPE {
REGULAR = 'regular',
TEAM = 'team',
COMMUNITY = 'community',
ENTERPRISE = 'enterprise',

View File

@ -1,6 +1,7 @@
// https://github.com/Hopding/pdf-lib/issues/20#issuecomment-412852821
import fontkit from '@pdf-lib/fontkit';
import { PDFDocument } from 'pdf-lib';
import { PDFDocument, RotationTypes, degrees, radiansToDegrees } from 'pdf-lib';
import { match } from 'ts-pattern';
import {
DEFAULT_HANDWRITING_FONT_SIZE,
@ -37,7 +38,32 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
throw new Error(`Page ${field.page} does not exist`);
}
const { width: pageWidth, height: pageHeight } = page.getSize();
const pageRotation = page.getRotation();
let pageRotationInDegrees = match(pageRotation.type)
.with(RotationTypes.Degrees, () => pageRotation.angle)
.with(RotationTypes.Radians, () => radiansToDegrees(pageRotation.angle))
.exhaustive();
// Round to the closest multiple of 90 degrees.
pageRotationInDegrees = Math.round(pageRotationInDegrees / 90) * 90;
const isPageRotatedToLandscape = pageRotationInDegrees === 90 || pageRotationInDegrees === 270;
let { width: pageWidth, height: pageHeight } = page.getSize();
// PDFs can have pages that are rotated, which are correctly rendered in the frontend.
// However when we load the PDF in the backend, the rotation is applied.
//
// To account for this, we swap the width and height for pages that are rotated by 90/270
// degrees. This is so we can calculate the virtual position the field was placed if it
// was correctly oriented in the frontend.
//
// Then when we insert the fields, we apply a transformation to the position of the field
// so it is rotated correctly.
if (isPageRotatedToLandscape) {
[pageWidth, pageHeight] = [pageHeight, pageWidth];
}
const fieldWidth = pageWidth * (Number(field.width) / 100);
const fieldHeight = pageHeight * (Number(field.height) / 100);
@ -65,17 +91,31 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
imageWidth = imageWidth * scalingFactor;
imageHeight = imageHeight * scalingFactor;
const imageX = fieldX + (fieldWidth - imageWidth) / 2;
let imageX = fieldX + (fieldWidth - imageWidth) / 2;
let imageY = fieldY + (fieldHeight - imageHeight) / 2;
// Invert the Y axis since PDFs use a bottom-left coordinate system
imageY = pageHeight - imageY - imageHeight;
if (pageRotationInDegrees !== 0) {
const adjustedPosition = adjustPositionForRotation(
pageWidth,
pageHeight,
imageX,
imageY,
pageRotationInDegrees,
);
imageX = adjustedPosition.xPos;
imageY = adjustedPosition.yPos;
}
page.drawImage(image, {
x: imageX,
y: imageY,
width: imageWidth,
height: imageHeight,
rotate: degrees(pageRotationInDegrees),
});
} else {
const longestLineInTextForWidth = field.customText
@ -90,17 +130,31 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
fontSize = Math.max(Math.min(fontSize * scalingFactor, maxFontSize), minFontSize);
textWidth = font.widthOfTextAtSize(longestLineInTextForWidth, fontSize);
const textX = fieldX + (fieldWidth - textWidth) / 2;
let textX = fieldX + (fieldWidth - textWidth) / 2;
let textY = fieldY + (fieldHeight - textHeight) / 2;
// Invert the Y axis since PDFs use a bottom-left coordinate system
textY = pageHeight - textY - textHeight;
if (pageRotationInDegrees !== 0) {
const adjustedPosition = adjustPositionForRotation(
pageWidth,
pageHeight,
textX,
textY,
pageRotationInDegrees,
);
textX = adjustedPosition.xPos;
textY = adjustedPosition.yPos;
}
page.drawText(field.customText, {
x: textX,
y: textY,
size: fontSize,
font,
rotate: degrees(pageRotationInDegrees),
});
}
@ -117,3 +171,32 @@ export const insertFieldInPDFBytes = async (
return await pdfDoc.save();
};
const adjustPositionForRotation = (
pageWidth: number,
pageHeight: number,
xPos: number,
yPos: number,
pageRotationInDegrees: number,
) => {
if (pageRotationInDegrees === 270) {
xPos = pageWidth - xPos;
[xPos, yPos] = [yPos, xPos];
}
if (pageRotationInDegrees === 90) {
yPos = pageHeight - yPos;
[xPos, yPos] = [yPos, xPos];
}
// Invert all the positions since it's rotated by 180 degrees.
if (pageRotationInDegrees === 180) {
xPos = pageWidth - xPos;
yPos = pageHeight - yPos;
}
return {
xPos,
yPos,
};
};

View File

@ -480,6 +480,7 @@ export const createDocumentFromDirectTemplate = async ({
// Send email to template owner.
const emailTemplate = createElement(DocumentCreatedFromDirectTemplateEmailTemplate, {
recipientName: directRecipientEmail,
recipientRole: directTemplateRecipient.role,
documentLink: `${formatDocumentsPath(document.team?.url)}/${document.id}`,
documentName: document.title,
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000',