mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 00:03:33 +10:00
Merge branch 'main' into feat/webhook-implementation
This commit is contained in:
@ -5,7 +5,7 @@ import { match } from 'ts-pattern';
|
|||||||
|
|
||||||
import { getStripeCustomerByUser } from '@documenso/ee/server-only/stripe/get-customer';
|
import { getStripeCustomerByUser } from '@documenso/ee/server-only/stripe/get-customer';
|
||||||
import { getPricesByInterval } from '@documenso/ee/server-only/stripe/get-prices-by-interval';
|
import { getPricesByInterval } from '@documenso/ee/server-only/stripe/get-prices-by-interval';
|
||||||
import { getPricesByPlan } from '@documenso/ee/server-only/stripe/get-prices-by-plan';
|
import { getPrimaryAccountPlanPrices } from '@documenso/ee/server-only/stripe/get-primary-account-plan-prices';
|
||||||
import { getProductByPriceId } from '@documenso/ee/server-only/stripe/get-product-by-price-id';
|
import { getProductByPriceId } from '@documenso/ee/server-only/stripe/get-product-by-price-id';
|
||||||
import { STRIPE_PLAN_TYPE } from '@documenso/lib/constants/billing';
|
import { STRIPE_PLAN_TYPE } from '@documenso/lib/constants/billing';
|
||||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
||||||
@ -37,23 +37,23 @@ export default async function BillingSettingsPage() {
|
|||||||
user = await getStripeCustomerByUser(user).then((result) => result.user);
|
user = await getStripeCustomerByUser(user).then((result) => result.user);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [subscriptions, prices, communityPlanPrices] = await Promise.all([
|
const [subscriptions, prices, primaryAccountPlanPrices] = await Promise.all([
|
||||||
getSubscriptionsByUserId({ userId: user.id }),
|
getSubscriptionsByUserId({ userId: user.id }),
|
||||||
getPricesByInterval({ plan: STRIPE_PLAN_TYPE.COMMUNITY }),
|
getPricesByInterval({ plan: STRIPE_PLAN_TYPE.COMMUNITY }),
|
||||||
getPricesByPlan(STRIPE_PLAN_TYPE.COMMUNITY),
|
getPrimaryAccountPlanPrices(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const communityPlanPriceIds = communityPlanPrices.map(({ id }) => id);
|
const primaryAccountPlanPriceIds = primaryAccountPlanPrices.map(({ id }) => id);
|
||||||
|
|
||||||
let subscriptionProduct: Stripe.Product | null = null;
|
let subscriptionProduct: Stripe.Product | null = null;
|
||||||
|
|
||||||
const communityPlanUserSubscriptions = subscriptions.filter(({ priceId }) =>
|
const primaryAccountPlanSubscriptions = subscriptions.filter(({ priceId }) =>
|
||||||
communityPlanPriceIds.includes(priceId),
|
primaryAccountPlanPriceIds.includes(priceId),
|
||||||
);
|
);
|
||||||
|
|
||||||
const subscription =
|
const subscription =
|
||||||
communityPlanUserSubscriptions.find(({ status }) => status === SubscriptionStatus.ACTIVE) ??
|
primaryAccountPlanSubscriptions.find(({ status }) => status === SubscriptionStatus.ACTIVE) ??
|
||||||
communityPlanUserSubscriptions[0];
|
primaryAccountPlanSubscriptions[0];
|
||||||
|
|
||||||
if (subscription?.priceId) {
|
if (subscription?.priceId) {
|
||||||
subscriptionProduct = await getProductByPriceId({ priceId: subscription.priceId }).catch(
|
subscriptionProduct = await getProductByPriceId({ priceId: subscription.priceId }).catch(
|
||||||
|
|||||||
@ -67,7 +67,7 @@ export const TeamsMemberPageDataTable = ({
|
|||||||
<Tabs value={currentTab} className="flex-shrink-0 overflow-x-auto">
|
<Tabs value={currentTab} className="flex-shrink-0 overflow-x-auto">
|
||||||
<TabsList>
|
<TabsList>
|
||||||
<TabsTrigger className="min-w-[60px]" value="members" asChild>
|
<TabsTrigger className="min-w-[60px]" value="members" asChild>
|
||||||
<Link href={pathname ?? '/'}>All</Link>
|
<Link href={pathname ?? '/'}>Active</Link>
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
|
||||||
<TabsTrigger className="min-w-[60px]" value="invites" asChild>
|
<TabsTrigger className="min-w-[60px]" value="invites" asChild>
|
||||||
|
|||||||
@ -1,11 +1,10 @@
|
|||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||||
import { STRIPE_PLAN_TYPE } from '@documenso/lib/constants/billing';
|
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { SubscriptionStatus } from '@documenso/prisma/client';
|
import { SubscriptionStatus } from '@documenso/prisma/client';
|
||||||
|
|
||||||
import { getPricesByPlan } from '../stripe/get-prices-by-plan';
|
import { getDocumentRelatedPrices } from '../stripe/get-document-related-prices.ts';
|
||||||
import { FREE_PLAN_LIMITS, SELFHOSTED_PLAN_LIMITS, TEAM_PLAN_LIMITS } from './constants';
|
import { FREE_PLAN_LIMITS, SELFHOSTED_PLAN_LIMITS, TEAM_PLAN_LIMITS } from './constants';
|
||||||
import { ERROR_CODES } from './errors';
|
import { ERROR_CODES } from './errors';
|
||||||
import { ZLimitsSchema } from './schema';
|
import { ZLimitsSchema } from './schema';
|
||||||
@ -56,10 +55,11 @@ const handleUserLimits = async ({ email }: HandleUserLimitsOptions) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (activeSubscriptions.length > 0) {
|
if (activeSubscriptions.length > 0) {
|
||||||
const communityPlanPrices = await getPricesByPlan(STRIPE_PLAN_TYPE.COMMUNITY);
|
const documentPlanPrices = await getDocumentRelatedPrices();
|
||||||
|
|
||||||
for (const subscription of activeSubscriptions) {
|
for (const subscription of activeSubscriptions) {
|
||||||
const price = communityPlanPrices.find((price) => price.id === subscription.priceId);
|
const price = documentPlanPrices.find((price) => price.id === subscription.priceId);
|
||||||
|
|
||||||
if (!price || typeof price.product === 'string' || price.product.deleted) {
|
if (!price || typeof price.product === 'string' || price.product.deleted) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,10 @@
|
|||||||
|
import { STRIPE_PLAN_TYPE } from '@documenso/lib/constants/billing';
|
||||||
|
|
||||||
|
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]);
|
||||||
|
};
|
||||||
13
packages/ee/server-only/stripe/get-enterprise-plan-prices.ts
Normal file
13
packages/ee/server-only/stripe/get-enterprise-plan-prices.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { STRIPE_PLAN_TYPE } from '@documenso/lib/constants/billing';
|
||||||
|
|
||||||
|
import { getPricesByPlan } from './get-prices-by-plan';
|
||||||
|
|
||||||
|
export const getEnterprisePlanPrices = async () => {
|
||||||
|
return await getPricesByPlan(STRIPE_PLAN_TYPE.ENTERPRISE);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getEnterprisePlanPriceIds = async () => {
|
||||||
|
const prices = await getEnterprisePlanPrices();
|
||||||
|
|
||||||
|
return prices.map((price) => price.id);
|
||||||
|
};
|
||||||
@ -1,14 +1,18 @@
|
|||||||
import type { STRIPE_PLAN_TYPE } from '@documenso/lib/constants/billing';
|
import type { STRIPE_PLAN_TYPE } from '@documenso/lib/constants/billing';
|
||||||
import { stripe } from '@documenso/lib/server-only/stripe';
|
import { stripe } from '@documenso/lib/server-only/stripe';
|
||||||
|
|
||||||
export const getPricesByPlan = async (
|
type PlanType = (typeof STRIPE_PLAN_TYPE)[keyof typeof STRIPE_PLAN_TYPE];
|
||||||
plan: (typeof STRIPE_PLAN_TYPE)[keyof typeof STRIPE_PLAN_TYPE],
|
|
||||||
) => {
|
export const getPricesByPlan = async (plan: PlanType | PlanType[]) => {
|
||||||
|
const planTypes = typeof plan === 'string' ? [plan] : plan;
|
||||||
|
|
||||||
|
const query = planTypes.map((planType) => `metadata['plan']:'${planType}'`).join(' OR ');
|
||||||
|
|
||||||
const { data: prices } = await stripe.prices.search({
|
const { data: prices } = await stripe.prices.search({
|
||||||
query: `metadata['plan']:'${plan}' type:'recurring'`,
|
query,
|
||||||
expand: ['data.product'],
|
expand: ['data.product'],
|
||||||
limit: 100,
|
limit: 100,
|
||||||
});
|
});
|
||||||
|
|
||||||
return prices;
|
return prices.filter((price) => price.type === 'recurring');
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,10 @@
|
|||||||
|
import { STRIPE_PLAN_TYPE } from '@documenso/lib/constants/billing';
|
||||||
|
|
||||||
|
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]);
|
||||||
|
};
|
||||||
17
packages/ee/server-only/stripe/get-team-related-prices.ts
Normal file
17
packages/ee/server-only/stripe/get-team-related-prices.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { STRIPE_PLAN_TYPE } from '@documenso/lib/constants/billing';
|
||||||
|
|
||||||
|
import { getPricesByPlan } from './get-prices-by-plan';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the Stripe prices of items that affect the amount of teams a user can create.
|
||||||
|
*/
|
||||||
|
export const getTeamRelatedPrices = async () => {
|
||||||
|
return await getPricesByPlan([STRIPE_PLAN_TYPE.COMMUNITY, STRIPE_PLAN_TYPE.ENTERPRISE]);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the Stripe price IDs of items that affect the amount of teams a user can create.
|
||||||
|
*/
|
||||||
|
export const getTeamRelatedPriceIds = async () => {
|
||||||
|
return await getTeamRelatedPrices().then((prices) => prices.map((price) => price.id));
|
||||||
|
};
|
||||||
@ -2,13 +2,13 @@ import type Stripe from 'stripe';
|
|||||||
|
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import { stripe } from '@documenso/lib/server-only/stripe';
|
import { stripe } from '@documenso/lib/server-only/stripe';
|
||||||
import { subscriptionsContainsActiveCommunityPlan } from '@documenso/lib/utils/billing';
|
import { subscriptionsContainsActivePlan } from '@documenso/lib/utils/billing';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { type Subscription, type Team, type User } from '@documenso/prisma/client';
|
import { type Subscription, type Team, type User } from '@documenso/prisma/client';
|
||||||
|
|
||||||
import { deleteCustomerPaymentMethods } from './delete-customer-payment-methods';
|
import { deleteCustomerPaymentMethods } from './delete-customer-payment-methods';
|
||||||
import { getCommunityPlanPriceIds } from './get-community-plan-prices';
|
|
||||||
import { getTeamPrices } from './get-team-prices';
|
import { getTeamPrices } from './get-team-prices';
|
||||||
|
import { getTeamRelatedPriceIds } from './get-team-related-prices';
|
||||||
|
|
||||||
type TransferStripeSubscriptionOptions = {
|
type TransferStripeSubscriptionOptions = {
|
||||||
/**
|
/**
|
||||||
@ -46,14 +46,14 @@ export const transferTeamSubscription = async ({
|
|||||||
throw new AppError(AppErrorCode.NOT_FOUND, 'Missing customer ID.');
|
throw new AppError(AppErrorCode.NOT_FOUND, 'Missing customer ID.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const [communityPlanIds, teamSeatPrices] = await Promise.all([
|
const [teamRelatedPlanPriceIds, teamSeatPrices] = await Promise.all([
|
||||||
getCommunityPlanPriceIds(),
|
getTeamRelatedPriceIds(),
|
||||||
getTeamPrices(),
|
getTeamPrices(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const teamSubscriptionRequired = !subscriptionsContainsActiveCommunityPlan(
|
const teamSubscriptionRequired = !subscriptionsContainsActivePlan(
|
||||||
user.Subscription,
|
user.Subscription,
|
||||||
communityPlanIds,
|
teamRelatedPlanPriceIds,
|
||||||
);
|
);
|
||||||
|
|
||||||
let teamSubscription: Stripe.Subscription | null = null;
|
let teamSubscription: Stripe.Subscription | null = null;
|
||||||
|
|||||||
13
packages/lib/client-only/hooks/use-effect-once.ts
Normal file
13
packages/lib/client-only/hooks/use-effect-once.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import type { EffectCallback } from 'react';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dangerously runs an effect "once" by ignoring the depedencies of a given effect.
|
||||||
|
*
|
||||||
|
* DANGER: The effect will run twice in concurrent react and development environments.
|
||||||
|
*/
|
||||||
|
export const unsafe_useEffectOnce = (callback: EffectCallback) => {
|
||||||
|
// Intentionally avoiding exhaustive deps and rule of hooks here
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps, react-hooks/rules-of-hooks
|
||||||
|
return useEffect(callback, []);
|
||||||
|
};
|
||||||
@ -3,18 +3,17 @@ import { env } from 'next-runtime-env';
|
|||||||
export const APP_DOCUMENT_UPLOAD_SIZE_LIMIT =
|
export const APP_DOCUMENT_UPLOAD_SIZE_LIMIT =
|
||||||
Number(process.env.NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT) || 50;
|
Number(process.env.NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT) || 50;
|
||||||
|
|
||||||
export const NEXT_PUBLIC_PROJECT = () => env('NEXT_PUBLIC_PROJECT');
|
|
||||||
export const NEXT_PUBLIC_WEBAPP_URL = () => env('NEXT_PUBLIC_WEBAPP_URL');
|
export const NEXT_PUBLIC_WEBAPP_URL = () => env('NEXT_PUBLIC_WEBAPP_URL');
|
||||||
export const NEXT_PUBLIC_MARKETING_URL = () => env('NEXT_PUBLIC_MARKETING_URL');
|
export const NEXT_PUBLIC_MARKETING_URL = () => env('NEXT_PUBLIC_MARKETING_URL');
|
||||||
|
|
||||||
export const IS_APP_MARKETING = () => NEXT_PUBLIC_PROJECT() === 'marketing';
|
export const IS_APP_MARKETING = process.env.NEXT_PUBLIC_PROJECT === 'marketing';
|
||||||
export const IS_APP_WEB = () => NEXT_PUBLIC_PROJECT() === 'web';
|
export const IS_APP_WEB = process.env.NEXT_PUBLIC_PROJECT === 'web';
|
||||||
export const IS_BILLING_ENABLED = () => env('NEXT_PUBLIC_FEATURE_BILLING_ENABLED') === 'true';
|
export const IS_BILLING_ENABLED = () => env('NEXT_PUBLIC_FEATURE_BILLING_ENABLED') === 'true';
|
||||||
|
|
||||||
export const APP_FOLDER = () => (IS_APP_MARKETING() ? 'marketing' : 'web');
|
export const APP_FOLDER = () => (IS_APP_MARKETING ? 'marketing' : 'web');
|
||||||
|
|
||||||
export const APP_BASE_URL = () =>
|
export const APP_BASE_URL = () =>
|
||||||
IS_APP_WEB() ? NEXT_PUBLIC_WEBAPP_URL() : NEXT_PUBLIC_MARKETING_URL();
|
IS_APP_WEB ? NEXT_PUBLIC_WEBAPP_URL() : NEXT_PUBLIC_MARKETING_URL();
|
||||||
|
|
||||||
export const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL() ?? 'http://localhost:3000';
|
export const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL() ?? 'http://localhost:3000';
|
||||||
export const MARKETING_BASE_URL = NEXT_PUBLIC_MARKETING_URL() ?? 'http://localhost:3001';
|
export const MARKETING_BASE_URL = NEXT_PUBLIC_MARKETING_URL() ?? 'http://localhost:3001';
|
||||||
|
|||||||
@ -6,6 +6,5 @@ export enum STRIPE_CUSTOMER_TYPE {
|
|||||||
export enum STRIPE_PLAN_TYPE {
|
export enum STRIPE_PLAN_TYPE {
|
||||||
TEAM = 'team',
|
TEAM = 'team',
|
||||||
COMMUNITY = 'community',
|
COMMUNITY = 'community',
|
||||||
|
ENTERPRISE = 'enterprise',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TEAM_BILLING_DOMAIN = 'billing.team.documenso.com';
|
|
||||||
|
|||||||
@ -2,11 +2,11 @@ import type Stripe from 'stripe';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { createTeamCustomer } from '@documenso/ee/server-only/stripe/create-team-customer';
|
import { createTeamCustomer } from '@documenso/ee/server-only/stripe/create-team-customer';
|
||||||
import { getCommunityPlanPriceIds } from '@documenso/ee/server-only/stripe/get-community-plan-prices';
|
import { getTeamRelatedPrices } from '@documenso/ee/server-only/stripe/get-team-related-prices';
|
||||||
import { mapStripeSubscriptionToPrismaUpsertAction } from '@documenso/ee/server-only/stripe/webhook/on-subscription-updated';
|
import { mapStripeSubscriptionToPrismaUpsertAction } from '@documenso/ee/server-only/stripe/webhook/on-subscription-updated';
|
||||||
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import { subscriptionsContainsActiveCommunityPlan } from '@documenso/lib/utils/billing';
|
import { subscriptionsContainsActivePlan } from '@documenso/lib/utils/billing';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { Prisma, TeamMemberRole } from '@documenso/prisma/client';
|
import { Prisma, TeamMemberRole } from '@documenso/prisma/client';
|
||||||
|
|
||||||
@ -61,13 +61,12 @@ export const createTeam = async ({
|
|||||||
let customerId: string | null = null;
|
let customerId: string | null = null;
|
||||||
|
|
||||||
if (IS_BILLING_ENABLED()) {
|
if (IS_BILLING_ENABLED()) {
|
||||||
const communityPlanPriceIds = await getCommunityPlanPriceIds();
|
const teamRelatedPriceIds = await getTeamRelatedPrices().then((prices) =>
|
||||||
|
prices.map((price) => price.id),
|
||||||
isPaymentRequired = !subscriptionsContainsActiveCommunityPlan(
|
|
||||||
user.Subscription,
|
|
||||||
communityPlanPriceIds,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
isPaymentRequired = !subscriptionsContainsActivePlan(user.Subscription, teamRelatedPriceIds);
|
||||||
|
|
||||||
customerId = await createTeamCustomer({
|
customerId = await createTeamCustomer({
|
||||||
name: user.name ?? teamName,
|
name: user.name ?? teamName,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
|
|||||||
@ -2,15 +2,14 @@ import type { Subscription } from '.prisma/client';
|
|||||||
import { SubscriptionStatus } from '.prisma/client';
|
import { SubscriptionStatus } from '.prisma/client';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if there is a subscription that is active and is a community plan.
|
* Returns true if there is a subscription that is active and is one of the provided price IDs.
|
||||||
*/
|
*/
|
||||||
export const subscriptionsContainsActiveCommunityPlan = (
|
export const subscriptionsContainsActivePlan = (
|
||||||
subscriptions: Subscription[],
|
subscriptions: Subscription[],
|
||||||
communityPlanPriceIds: string[],
|
priceIds: string[],
|
||||||
) => {
|
) => {
|
||||||
return subscriptions.some(
|
return subscriptions.some(
|
||||||
(subscription) =>
|
(subscription) =>
|
||||||
subscription.status === SubscriptionStatus.ACTIVE &&
|
subscription.status === SubscriptionStatus.ACTIVE && priceIds.includes(subscription.priceId),
|
||||||
communityPlanPriceIds.includes(subscription.priceId),
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -7,6 +7,8 @@ import { Undo2 } from 'lucide-react';
|
|||||||
import type { StrokeOptions } from 'perfect-freehand';
|
import type { StrokeOptions } from 'perfect-freehand';
|
||||||
import { getStroke } from 'perfect-freehand';
|
import { getStroke } from 'perfect-freehand';
|
||||||
|
|
||||||
|
import { unsafe_useEffectOnce } from '@documenso/lib/client-only/hooks/use-effect-once';
|
||||||
|
|
||||||
import { cn } from '../../lib/utils';
|
import { cn } from '../../lib/utils';
|
||||||
import { getSvgPathFromStroke } from './helper';
|
import { getSvgPathFromStroke } from './helper';
|
||||||
import { Point } from './point';
|
import { Point } from './point';
|
||||||
@ -28,6 +30,7 @@ export const SignaturePad = ({
|
|||||||
...props
|
...props
|
||||||
}: SignaturePadProps) => {
|
}: SignaturePadProps) => {
|
||||||
const $el = useRef<HTMLCanvasElement>(null);
|
const $el = useRef<HTMLCanvasElement>(null);
|
||||||
|
const $imageData = useRef<ImageData | null>(null);
|
||||||
|
|
||||||
const [isPressed, setIsPressed] = useState(false);
|
const [isPressed, setIsPressed] = useState(false);
|
||||||
const [lines, setLines] = useState<Point[][]>([]);
|
const [lines, setLines] = useState<Point[][]>([]);
|
||||||
@ -134,7 +137,6 @@ export const SignaturePad = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
onChange?.($el.current.toDataURL());
|
onChange?.($el.current.toDataURL());
|
||||||
|
|
||||||
ctx.save();
|
ctx.save();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -163,6 +165,7 @@ export const SignaturePad = ({
|
|||||||
const ctx = $el.current.getContext('2d');
|
const ctx = $el.current.getContext('2d');
|
||||||
|
|
||||||
ctx?.clearRect(0, 0, $el.current.width, $el.current.height);
|
ctx?.clearRect(0, 0, $el.current.width, $el.current.height);
|
||||||
|
$imageData.current = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
onChange?.(null);
|
onChange?.(null);
|
||||||
@ -176,19 +179,25 @@ export const SignaturePad = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newLines = [...lines];
|
const newLines = lines.slice(0, -1);
|
||||||
newLines.pop(); // Remove the last line
|
|
||||||
setLines(newLines);
|
setLines(newLines);
|
||||||
|
|
||||||
// Clear the canvas
|
// Clear the canvas
|
||||||
if ($el.current) {
|
if ($el.current) {
|
||||||
const ctx = $el.current.getContext('2d');
|
const ctx = $el.current.getContext('2d');
|
||||||
ctx?.clearRect(0, 0, $el.current.width, $el.current.height);
|
const { width, height } = $el.current;
|
||||||
|
ctx?.clearRect(0, 0, width, height);
|
||||||
|
|
||||||
|
if (typeof defaultValue === 'string' && $imageData.current) {
|
||||||
|
ctx?.putImageData($imageData.current, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
newLines.forEach((line) => {
|
newLines.forEach((line) => {
|
||||||
const pathData = new Path2D(getSvgPathFromStroke(getStroke(line, perfectFreehandOptions)));
|
const pathData = new Path2D(getSvgPathFromStroke(getStroke(line, perfectFreehandOptions)));
|
||||||
ctx?.fill(pathData);
|
ctx?.fill(pathData);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onChange?.($el.current.toDataURL());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -199,7 +208,7 @@ export const SignaturePad = ({
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
unsafe_useEffectOnce(() => {
|
||||||
if ($el.current && typeof defaultValue === 'string') {
|
if ($el.current && typeof defaultValue === 'string') {
|
||||||
const ctx = $el.current.getContext('2d');
|
const ctx = $el.current.getContext('2d');
|
||||||
|
|
||||||
@ -209,11 +218,15 @@ export const SignaturePad = ({
|
|||||||
|
|
||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
ctx?.drawImage(img, 0, 0, Math.min(width, img.width), Math.min(height, img.height));
|
ctx?.drawImage(img, 0, 0, Math.min(width, img.width), Math.min(height, img.height));
|
||||||
|
|
||||||
|
const defaultImageData = ctx?.getImageData(0, 0, width, height) || null;
|
||||||
|
|
||||||
|
$imageData.current = defaultImageData;
|
||||||
};
|
};
|
||||||
|
|
||||||
img.src = defaultValue;
|
img.src = defaultValue;
|
||||||
}
|
}
|
||||||
}, [defaultValue]);
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
Reference in New Issue
Block a user