♻️ Use user tier to determine subscription status (#1686)

This commit is contained in:
Luke Vella 2025-04-24 10:35:23 +01:00 committed by GitHub
parent 5c61057a84
commit cd38e9105d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 9 additions and 71 deletions

View file

@ -17,7 +17,8 @@ import { Trans } from "react-i18next";
import { PayWallDialog } from "@/components/pay-wall-dialog"; import { PayWallDialog } from "@/components/pay-wall-dialog";
import { ProBadge } from "@/components/pro-badge"; import { ProBadge } from "@/components/pro-badge";
import { usePlan } from "@/contexts/plan";
import { useUser } from "../user-provider";
export type PollSettingsFormData = { export type PollSettingsFormData = {
requireParticipantEmail: boolean; requireParticipantEmail: boolean;
@ -66,9 +67,9 @@ export const PollSettingsForm = ({ children }: React.PropsWithChildren) => {
const form = useFormContext<PollSettingsFormData>(); const form = useFormContext<PollSettingsFormData>();
const posthog = usePostHog(); const posthog = usePostHog();
const paywallDialog = useDialog(); const paywallDialog = useDialog();
const plan = usePlan(); const { user } = useUser();
const isFree = plan === "free"; const isFree = user.tier !== "pro";
return ( return (
<> <>

View file

@ -32,10 +32,10 @@ import { PayWallDialog } from "@/components/pay-wall-dialog";
import { FinalizePollDialog } from "@/components/poll/manage-poll/finalize-poll-dialog"; import { FinalizePollDialog } from "@/components/poll/manage-poll/finalize-poll-dialog";
import { ProBadge } from "@/components/pro-badge"; import { ProBadge } from "@/components/pro-badge";
import { Trans } from "@/components/trans"; import { Trans } from "@/components/trans";
import { usePlan } from "@/contexts/plan";
import { usePoll } from "@/contexts/poll"; import { usePoll } from "@/contexts/poll";
import { trpc } from "@/trpc/client"; import { trpc } from "@/trpc/client";
import { useUser } from "../user-provider";
import { DeletePollDialog } from "./manage-poll/delete-poll-dialog"; import { DeletePollDialog } from "./manage-poll/delete-poll-dialog";
import { useCsvExporter } from "./manage-poll/use-csv-exporter"; import { useCsvExporter } from "./manage-poll/use-csv-exporter";
@ -125,6 +125,7 @@ const ManagePoll: React.FunctionComponent<{
}> = ({ disabled }) => { }> = ({ disabled }) => {
const poll = usePoll(); const poll = usePoll();
const queryClient = trpc.useUtils(); const queryClient = trpc.useUtils();
const { user } = useUser();
const reopen = trpc.polls.reopen.useMutation({ const reopen = trpc.polls.reopen.useMutation({
onMutate: () => { onMutate: () => {
queryClient.polls.get.setData({ urlId: poll.id }, (oldPoll) => { queryClient.polls.get.setData({ urlId: poll.id }, (oldPoll) => {
@ -143,7 +144,6 @@ const ManagePoll: React.FunctionComponent<{
const duplicateDialog = useDialog(); const duplicateDialog = useDialog();
const finalizeDialog = useDialog(); const finalizeDialog = useDialog();
const paywallDialog = useDialog(); const paywallDialog = useDialog();
const plan = usePlan();
const posthog = usePostHog(); const posthog = usePostHog();
const { exportToCsv } = useCsvExporter(); const { exportToCsv } = useCsvExporter();
@ -203,7 +203,7 @@ const ManagePoll: React.FunctionComponent<{
<DropdownMenuItem <DropdownMenuItem
disabled={!!poll.event} disabled={!!poll.event}
onClick={() => { onClick={() => {
if (plan === "free") { if (user.tier !== "pro") {
paywallDialog.trigger(); paywallDialog.trigger();
posthog?.capture("trigger paywall", { posthog?.capture("trigger paywall", {
poll_id: poll.id, poll_id: poll.id,
@ -233,7 +233,7 @@ const ManagePoll: React.FunctionComponent<{
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => { onClick={() => {
if (plan === "free") { if (user.tier !== "pro") {
paywallDialog.trigger(); paywallDialog.trigger();
posthog?.capture("trigger paywall", { posthog?.capture("trigger paywall", {
poll_id: poll.id, poll_id: poll.id,

View file

@ -31,14 +31,12 @@ import { OptimizedAvatarImage } from "@/components/optimized-avatar-image";
import { RegisterLink } from "@/components/register-link"; import { RegisterLink } from "@/components/register-link";
import { Trans } from "@/components/trans"; import { Trans } from "@/components/trans";
import { IfCloudHosted, IfSelfHosted } from "@/contexts/environment"; import { IfCloudHosted, IfSelfHosted } from "@/contexts/environment";
import { Plan, usePlan } from "@/contexts/plan";
import { isFeedbackEnabled } from "@/utils/constants"; import { isFeedbackEnabled } from "@/utils/constants";
import { IfAuthenticated, IfGuest, useUser } from "./user-provider"; import { IfAuthenticated, IfGuest, useUser } from "./user-provider";
export const UserDropdown = ({ className }: { className?: string }) => { export const UserDropdown = ({ className }: { className?: string }) => {
const { user, logout } = useUser(); const { user, logout } = useUser();
usePlan(); // prefetch plan data
return ( return (
<DropdownMenu modal={false}> <DropdownMenu modal={false}>
<DropdownMenuTrigger <DropdownMenuTrigger
@ -68,9 +66,6 @@ export const UserDropdown = ({ className }: { className?: string }) => {
</div> </div>
) : null} ) : null}
</div> </div>
<div className="ml-4">
<Plan />
</div>
</DropdownMenuLabel> </DropdownMenuLabel>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem asChild={true}> <DropdownMenuItem asChild={true}>

View file

@ -4,7 +4,6 @@ import { useRouter } from "next/navigation";
import { signIn, signOut } from "next-auth/react"; import { signIn, signOut } from "next-auth/react";
import React from "react"; import React from "react";
import { useSubscription } from "@/contexts/plan";
import { useTranslation } from "@/i18n/client"; import { useTranslation } from "@/i18n/client";
import { isOwner } from "@/utils/permissions"; import { isOwner } from "@/utils/permissions";
@ -77,13 +76,12 @@ export const UserProvider = ({
children?: React.ReactNode; children?: React.ReactNode;
user?: RegisteredUser | GuestUser; user?: RegisteredUser | GuestUser;
}) => { }) => {
const subscription = useSubscription();
const { t } = useTranslation(); const { t } = useTranslation();
const router = useRouter(); const router = useRouter();
const posthog = usePostHog(); const posthog = usePostHog();
const isGuest = !user || user.tier === "guest"; const isGuest = !user || user.tier === "guest";
const tier = isGuest ? "guest" : subscription?.active ? "pro" : "hobby"; const tier = isGuest ? "guest" : user.tier;
React.useEffect(() => { React.useEffect(() => {
if (user) { if (user) {

View file

@ -1,56 +0,0 @@
"use client";
import { Badge } from "@rallly/ui/badge";
import React from "react";
import { ProBadge } from "@/components/pro-badge";
import { Trans } from "@/components/trans";
import { trpc } from "@/trpc/client";
import { isSelfHosted } from "@/utils/constants";
export const useSubscription = () => {
const { data } = trpc.user.subscription.useQuery(undefined, {
enabled: !isSelfHosted,
});
if (isSelfHosted) {
return {
active: true,
};
}
return data;
};
export const usePlan = () => {
const data = useSubscription();
const isPaid = data?.active === true;
return isPaid ? "paid" : "free";
};
export const IfSubscribed = ({ children }: React.PropsWithChildren) => {
const plan = usePlan();
return plan === "paid" ? <>{children}</> : null;
};
export const IfFreeUser = ({ children }: React.PropsWithChildren) => {
const subscription = useSubscription();
return subscription?.active === false ? <>{children}</> : null;
};
export const Plan = () => {
const plan = usePlan();
if (plan === "paid") {
return <ProBadge />;
}
return (
<Badge>
<Trans i18nKey="planFree" defaults="Free" />
</Badge>
);
};