Skip to content

Commit 0ba87fb

Browse files
committed
custom claim for pro state
1 parent 41860c5 commit 0ba87fb

16 files changed

+192
-248
lines changed

apps/desktop/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
"dompurify": "^3.3.0",
8787
"effect": "^3.19.6",
8888
"json5": "^2.2.3",
89+
"jwt-decode": "^4.0.0",
8990
"lucide-react": "^0.544.0",
9091
"motion": "^11.18.2",
9192
"mutative": "^1.3.0",

apps/desktop/src/billing.tsx

Lines changed: 18 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,18 @@
1-
import { useQuery } from "@tanstack/react-query";
21
import { openUrl } from "@tauri-apps/plugin-opener";
2+
import { jwtDecode } from "jwt-decode";
33
import {
44
createContext,
55
type ReactNode,
66
useCallback,
77
useContext,
88
useMemo,
99
} from "react";
10-
import type Stripe from "stripe";
1110

1211
import { useAuth } from "./auth";
1312
import { env } from "./env";
1413

15-
type BillingRow = {
16-
id: string;
17-
user_id: string;
18-
created_at: string;
19-
updated_at: string;
20-
stripe_customer: Stripe.Customer | null;
21-
stripe_subscription: Stripe.Subscription | null;
22-
};
23-
24-
type BillingData = (BillingRow & { isPro: boolean }) | null;
25-
2614
type BillingContextValue = {
27-
data: BillingData;
2815
isPro: boolean;
29-
isLoading: boolean;
30-
isPending: boolean;
31-
isFetching: boolean;
32-
isRefetching: boolean;
33-
isError: boolean;
34-
error: unknown;
35-
refetch: () => Promise<unknown>;
3616
upgradeToPro: () => void;
3717
};
3818

@@ -43,75 +23,31 @@ const BillingContext = createContext<BillingContextValue | null>(null);
4323
export function BillingProvider({ children }: { children: ReactNode }) {
4424
const auth = useAuth();
4525

46-
const {
47-
data: queryData,
48-
isLoading,
49-
isPending,
50-
isFetching,
51-
isRefetching,
52-
isError,
53-
error,
54-
refetch,
55-
} = useQuery({
56-
enabled: !!auth?.supabase && !!auth?.session?.user?.id,
57-
queryKey: ["billing", auth?.session?.user?.id],
58-
queryFn: async (): Promise<BillingData> => {
59-
if (!auth?.supabase || !auth?.session?.user?.id) {
60-
return null;
61-
}
62-
63-
const { data, error } = await auth.supabase
64-
.from("billings")
65-
.select("*")
66-
.eq("user_id", auth.session.user.id)
67-
.maybeSingle();
68-
69-
if (error) {
70-
throw error;
71-
}
72-
73-
if (!data) {
74-
return null;
75-
}
76-
77-
const billing = data as BillingRow;
78-
return {
79-
...billing,
80-
isPro: computeIsPro(billing.stripe_subscription),
81-
};
82-
},
83-
});
84-
85-
const data = queryData ?? null;
26+
const isPro = useMemo(() => {
27+
if (!auth?.session?.access_token) {
28+
return false;
29+
}
30+
31+
try {
32+
const decoded = jwtDecode<{ is_pro?: boolean }>(
33+
auth.session.access_token,
34+
);
35+
return decoded.is_pro ?? false;
36+
} catch {
37+
return false;
38+
}
39+
}, [auth?.session?.access_token]);
8640

8741
const upgradeToPro = useCallback(() => {
8842
openUrl(`${env.VITE_APP_URL}/app/checkout?period=monthly`);
89-
}, [auth]);
43+
}, []);
9044

9145
const value = useMemo<BillingContextValue>(
9246
() => ({
93-
data,
94-
isPro: !!data?.isPro,
95-
isLoading,
96-
isPending,
97-
isFetching,
98-
isRefetching,
99-
isError,
100-
error,
101-
refetch: () => refetch(),
47+
isPro,
10248
upgradeToPro,
10349
}),
104-
[
105-
data,
106-
error,
107-
isError,
108-
isFetching,
109-
isLoading,
110-
isPending,
111-
isRefetching,
112-
refetch,
113-
upgradeToPro,
114-
],
50+
[isPro, upgradeToPro],
11551
);
11652

11753
return (
@@ -128,23 +64,3 @@ export function useBillingAccess() {
12864

12965
return context;
13066
}
131-
132-
function computeIsPro(
133-
subscription: Stripe.Subscription | null | undefined,
134-
): boolean {
135-
if (!subscription) {
136-
return false;
137-
}
138-
139-
const hasValidStatus = ["active", "trialing"].includes(subscription.status);
140-
141-
const hasProProduct = subscription.items.data.some((item) => {
142-
const product = item.price.product;
143-
144-
return typeof product === "string"
145-
? product === env.VITE_PRO_PRODUCT_ID
146-
: product.id === env.VITE_PRO_PRODUCT_ID;
147-
});
148-
149-
return hasValidStatus && hasProProduct;
150-
}

apps/desktop/src/components/settings/account.tsx

Lines changed: 57 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,50 @@
1+
import { useQuery } from "@tanstack/react-query";
12
import { openUrl } from "@tauri-apps/plugin-opener";
23
import { ExternalLinkIcon } from "lucide-react";
34
import { type ReactNode, useCallback, useEffect, useState } from "react";
4-
import type Stripe from "stripe";
55

66
import { Button } from "@hypr/ui/components/ui/button";
77
import { Input } from "@hypr/ui/components/ui/input";
88

99
import { useAuth } from "../../auth";
10-
import { useBillingAccess } from "../../billing";
1110
import { env } from "../../env";
1211

1312
const WEB_APP_BASE_URL = env.VITE_APP_URL ?? "http://localhost:3000";
1413

14+
type SubscriptionRow = {
15+
stripe_customer_id: string | null;
16+
subscription_id: string | null;
17+
subscription_status: string | null;
18+
current_period_start: string | null;
19+
current_period_end: string | null;
20+
cancel_at_period_end: boolean | null;
21+
};
22+
1523
export function SettingsAccount() {
1624
const auth = useAuth();
17-
const billing = useBillingAccess();
25+
26+
const { data: subscription } = useQuery({
27+
enabled: !!auth?.supabase && !!auth?.session?.user?.id,
28+
queryKey: ["subscription", auth?.session?.user?.id],
29+
queryFn: async (): Promise<SubscriptionRow | null> => {
30+
if (!auth?.supabase) {
31+
return null;
32+
}
33+
34+
const { data, error } = await auth.supabase
35+
.from("billing_with_subscription")
36+
.select(
37+
"stripe_customer_id, subscription_id, subscription_status, current_period_start, current_period_end, cancel_at_period_end",
38+
)
39+
.maybeSingle();
40+
41+
if (error) {
42+
throw error;
43+
}
44+
45+
return data as SubscriptionRow | null;
46+
},
47+
});
1848

1949
const isAuthenticated = !!auth?.session;
2050
const [isPending, setIsPending] = useState(false);
@@ -110,7 +140,7 @@ export function SettingsAccount() {
110140
);
111141
}
112142

113-
const hasStripeCustomer = !!billing.data?.stripe_customer;
143+
const hasStripeCustomer = !!subscription?.stripe_customer_id;
114144

115145
return (
116146
<div className="flex flex-col gap-4">
@@ -145,9 +175,11 @@ export function SettingsAccount() {
145175
) : undefined
146176
}
147177
>
148-
{billing.data?.stripe_subscription && (
178+
{subscription?.subscription_id && (
149179
<SubscriptionDetails
150-
subscription={billing.data.stripe_subscription}
180+
status={subscription.subscription_status}
181+
currentPeriodStart={subscription.current_period_start}
182+
currentPeriodEnd={subscription.current_period_end}
151183
/>
152184
)}
153185
</Container>
@@ -156,23 +188,32 @@ export function SettingsAccount() {
156188
}
157189

158190
function SubscriptionDetails({
159-
subscription,
191+
status,
192+
currentPeriodStart,
193+
currentPeriodEnd,
160194
}: {
161-
subscription: Stripe.Subscription;
195+
status: string | null;
196+
currentPeriodStart: string | null;
197+
currentPeriodEnd: string | null;
162198
}) {
163-
const {
164-
status,
165-
items: {
166-
data: [{ current_period_end, current_period_start }],
167-
},
168-
} = subscription;
199+
if (!status) {
200+
return null;
201+
}
202+
203+
if (!currentPeriodStart || !currentPeriodEnd) {
204+
return (
205+
<div className="flex flex-row gap-1 text-xs text-neutral-600">
206+
<span className="capitalize">{status}</span>
207+
</div>
208+
);
209+
}
169210

170211
return (
171212
<div className="flex flex-row gap-1 text-xs text-neutral-600">
172213
<span className="capitalize">{status}:</span>
173-
<span>{new Date(current_period_start * 1000).toLocaleDateString()}</span>
214+
<span>{new Date(currentPeriodStart).toLocaleDateString()}</span>
174215
<span>~</span>
175-
<span>{new Date(current_period_end * 1000).toLocaleDateString()}</span>
216+
<span>{new Date(currentPeriodEnd).toLocaleDateString()}</span>
176217
</div>
177218
);
178219
}

apps/desktop/src/components/settings/ai/llm/health.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,9 +127,7 @@ function useAvailability() {
127127
if (llmProviderRequiresPro(current_llm_provider) && !billing.isPro) {
128128
return {
129129
available: false,
130-
message: billing.isLoading
131-
? "Checking plan access for this provider..."
132-
: "Upgrade to Pro to use this provider.",
130+
message: "Upgrade to Pro to use this provider.",
133131
};
134132
}
135133

apps/desktop/src/components/settings/ai/stt/health.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,7 @@ function useAvailability():
9191
if (sttProviderRequiresPro(providerId) && !billing.isPro) {
9292
return {
9393
available: false,
94-
message: billing.isLoading
95-
? "Checking plan access for this provider..."
96-
: "Upgrade to Pro to use this provider.",
94+
message: "Upgrade to Pro to use this provider.",
9795
};
9896
}
9997

0 commit comments

Comments
 (0)