Skip to content

Commit 5c3a6e5

Browse files
authored
refactor: restrict publish count per account (#5030)
1 parent dbd4650 commit 5c3a6e5

File tree

10 files changed

+125
-25
lines changed

10 files changed

+125
-25
lines changed

apps/builder/app/builder/features/topbar/publish.tsx

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -242,13 +242,16 @@ const $usedProFeatures = computed(
242242

243243
const Publish = ({
244244
project,
245-
refresh,
245+
timesLeft,
246246
disabled,
247+
refresh,
247248
}: {
248249
project: Project;
249-
refresh: () => Promise<void>;
250+
timesLeft: number;
250251
disabled: boolean;
252+
refresh: () => Promise<void>;
251253
}) => {
254+
const { maxPublishesAllowedPerUser } = useStore($userPlanFeatures);
252255
const [publishError, setPublishError] = useState<
253256
undefined | JSX.Element | string
254257
>();
@@ -379,6 +382,19 @@ const Publish = ({
379382
};
380383

381384
if (status === "PUBLISHED") {
385+
toast.success(
386+
<>
387+
The project has been successfully published. The project is
388+
successfully published.{" "}
389+
{hasProPlan === false && (
390+
<div>
391+
On the free plan, you have {timesLeft} out of{" "}
392+
{maxPublishesAllowedPerUser} daily publications remaining. The
393+
counter resets tomorrow.
394+
</div>
395+
)}
396+
</>
397+
);
382398
break;
383399
}
384400

@@ -601,6 +617,18 @@ const useCanAddDomain = () => {
601617
return { canAddDomain, maxDomainsAllowedPerUser };
602618
};
603619

620+
const useUserPublishCount = () => {
621+
const { load, data } = trpcClient.project.userPublishCount.useQuery();
622+
const { maxPublishesAllowedPerUser } = useStore($userPlanFeatures);
623+
useEffect(() => {
624+
load();
625+
}, [load]);
626+
return {
627+
userPublishCount: data?.success ? data.data : 0,
628+
maxPublishesAllowedPerUser,
629+
};
630+
};
631+
604632
const refreshProject = async () => {
605633
const result = await nativeClient.domain.project.query(
606634
{
@@ -634,11 +662,29 @@ const Content = (props: {
634662
const projectState = "idle";
635663

636664
const { canAddDomain, maxDomainsAllowedPerUser } = useCanAddDomain();
665+
const { userPublishCount, maxPublishesAllowedPerUser } =
666+
useUserPublishCount();
637667

638668
return (
639669
<form>
640670
<ScrollArea>
641-
{usedProFeatures.size > 0 && hasProPlan === false ? (
671+
{userPublishCount >= maxPublishesAllowedPerUser ? (
672+
<PanelBanner>
673+
<Text variant="regularBold">
674+
Upgrade to publish more than {maxPublishesAllowedPerUser} times
675+
per day:
676+
</Text>
677+
<Link
678+
className={buttonStyle({ color: "gradient" })}
679+
color="contrast"
680+
underline="none"
681+
href="https://webstudio.is/pricing"
682+
target="_blank"
683+
>
684+
Upgrade
685+
</Link>
686+
</PanelBanner>
687+
) : usedProFeatures.size > 0 && hasProPlan === false ? (
642688
<PanelBanner>
643689
<img
644690
src={cmsUpgradeBanner}
@@ -719,7 +765,11 @@ const Content = (props: {
719765
<Publish
720766
project={project}
721767
refresh={refreshProject}
722-
disabled={usedProFeatures.size > 0 && hasProPlan === false}
768+
timesLeft={maxPublishesAllowedPerUser - userPublishCount}
769+
disabled={
770+
(usedProFeatures.size > 0 && hasProPlan === false) ||
771+
userPublishCount >= maxPublishesAllowedPerUser
772+
}
723773
/>
724774
</Flex>
725775
</form>

apps/builder/app/dashboard/dashboard.stories.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const userPlanFeatures: UserPlanFeatures = {
3232
allowDynamicData: false,
3333
maxContactEmails: 0,
3434
maxDomainsAllowedPerUser: 1,
35+
maxPublishesAllowedPerUser: 1,
3536
};
3637

3738
const projects = [

apps/builder/app/shared/db/user-plan-features.server.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export const getUserPlanFeatures = async (
2424
.select("name")
2525
.in(
2626
"id",
27-
userProducts.map(({ productId }) => productId)
27+
userProducts.map(({ productId }) => productId ?? "")
2828
);
2929

3030
if (productsResult.error) {
@@ -46,6 +46,7 @@ export const getUserPlanFeatures = async (
4646
allowDynamicData: true,
4747
maxContactEmails: 5,
4848
maxDomainsAllowedPerUser: Number.MAX_SAFE_INTEGER,
49+
maxPublishesAllowedPerUser: Number.MAX_SAFE_INTEGER,
4950
hasSubscription,
5051
hasProPlan: true,
5152
planName: products[0].name,
@@ -58,6 +59,7 @@ export const getUserPlanFeatures = async (
5859
allowDynamicData: true,
5960
maxContactEmails: 5,
6061
maxDomainsAllowedPerUser: Number.MAX_SAFE_INTEGER,
62+
maxPublishesAllowedPerUser: Number.MAX_SAFE_INTEGER,
6163
hasSubscription: true,
6264
hasProPlan: true,
6365
planName: "env.USER_PLAN Pro",
@@ -69,6 +71,7 @@ export const getUserPlanFeatures = async (
6971
allowDynamicData: false,
7072
maxContactEmails: 0,
7173
maxDomainsAllowedPerUser: 1,
74+
maxPublishesAllowedPerUser: 10,
7275
hasSubscription: false,
7376
hasProPlan: false,
7477
};

apps/builder/app/shared/nano-states/misc.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,7 @@ export const $userPlanFeatures = atom<UserPlanFeatures>({
317317
allowDynamicData: false,
318318
maxContactEmails: 0,
319319
maxDomainsAllowedPerUser: 1,
320+
maxPublishesAllowedPerUser: 1,
320321
hasSubscription: false,
321322
hasProPlan: false,
322323
});

packages/postgrest/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"db-test": "docker run --rm --network host -v ./supabase/tests:/tests -e PGOPTIONS='--search_path=pgtap,public' supabase/pg_prove:3.36 pg_prove -d ${DIRECT_URL:-postgresql://postgres:pass@localhost/webstudio} --ext .sql /tests"
2222
},
2323
"dependencies": {
24-
"@supabase/postgrest-js": "^1.16.2"
24+
"@supabase/postgrest-js": "^1.19.3"
2525
},
2626
"devDependencies": {
2727
"@webstudio-is/tsconfig": "workspace:*"

packages/postgrest/src/__generated__/db-types.ts

Lines changed: 19 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
CREATE OR REPLACE VIEW user_publish_count AS
2+
SELECT DISTINCT "userId" AS user_id, count(*)
3+
FROM "Build"
4+
LEFT JOIN "Project" ON "Project".id="Build"."projectId"
5+
WHERE DATE_TRUNC('day', "Build"."createdAt") = DATE_TRUNC('day', NOW())
6+
AND "Build".deployment IS NOT NULL
7+
GROUP BY "userId";

packages/project/src/trpc/project-router.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,38 @@ export const projectRouter = router({
6363

6464
return projectIds.map((project) => project.id);
6565
}),
66+
67+
userPublishCount: procedure.query(async ({ ctx }) => {
68+
try {
69+
if (
70+
ctx.authorization.type !== "user" &&
71+
ctx.authorization.type !== "token"
72+
) {
73+
throw new Error("Not authorized");
74+
}
75+
const userId =
76+
ctx.authorization.type === "user"
77+
? ctx.authorization.userId
78+
: ctx.authorization.ownerId;
79+
const result = await ctx.postgrest.client
80+
.from("user_publish_count")
81+
.select("count")
82+
.eq("user_id", userId)
83+
.maybeSingle();
84+
if (result.error) {
85+
throw result.error;
86+
}
87+
return {
88+
success: true,
89+
data: result.data?.count ?? 0,
90+
};
91+
} catch (error) {
92+
return {
93+
success: false,
94+
error: error instanceof Error ? error.message : "Unknown error",
95+
} as const;
96+
}
97+
}),
6698
});
6799

68100
export type ProjectRouter = typeof projectRouter;

packages/trpc-interface/src/context/context.server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ type UserPlanFeatures = {
6868
allowDynamicData: boolean;
6969
maxContactEmails: number;
7070
maxDomainsAllowedPerUser: number;
71+
maxPublishesAllowedPerUser: number;
7172
hasSubscription: boolean;
7273
} & (
7374
| {

pnpm-lock.yaml

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)