mirror of
https://github.com/documenso/documenso.git
synced 2025-11-20 11:41:44 +10:00
Merge branch 'main' into feat/public-profiles
This commit is contained in:
@ -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]);
|
||||
};
|
||||
|
||||
@ -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 = {}) => {
|
||||
|
||||
@ -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]);
|
||||
};
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
@ -4,6 +4,7 @@ export enum STRIPE_CUSTOMER_TYPE {
|
||||
}
|
||||
|
||||
export enum STRIPE_PLAN_TYPE {
|
||||
REGULAR = 'regular',
|
||||
TEAM = 'team',
|
||||
COMMUNITY = 'community',
|
||||
ENTERPRISE = 'enterprise',
|
||||
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@ -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',
|
||||
|
||||
Reference in New Issue
Block a user