diff --git a/src/components/blog/premium-content.tsx b/src/components/blog/premium-content.tsx
index e3b9ca8..e56fb1e 100644
--- a/src/components/blog/premium-content.tsx
+++ b/src/components/blog/premium-content.tsx
@@ -9,10 +9,24 @@ interface PremiumContentProps {
}
/**
- * This component will now rely on server-side filtering
- * The
tags will be removed server-side for non-premium users
- * This component only serves as a marker for premium sections
+ * Client-side Premium Content component
+ * Note: This component now serves as a fallback for client-side rendering.
+ * The main security filtering happens server-side in PremiumGuard component.
*/
export function PremiumContent({ children }: PremiumContentProps) {
+ const currentUser = useCurrentUser();
+ const { data: paymentData } = useCurrentPlan(currentUser?.id);
+
+ // Determine if user has premium access
+ const hasPremiumAccess =
+ paymentData?.currentPlan &&
+ (paymentData.currentPlan.isLifetime || !paymentData.currentPlan.isFree);
+
+ // Only show content if user has premium access
+ // This is a client-side fallback - main security is handled server-side
+ if (!currentUser || !hasPremiumAccess) {
+ return null;
+ }
+
return {children}
;
}
diff --git a/src/components/blog/premium-guard.tsx b/src/components/blog/premium-guard.tsx
index f9f6109..9462dcc 100644
--- a/src/components/blog/premium-guard.tsx
+++ b/src/components/blog/premium-guard.tsx
@@ -29,6 +29,14 @@ export function PremiumGuard({
canAccess,
className,
}: PremiumGuardProps) {
+ // All hooks must be called unconditionally at the top
+ const t = useTranslations('BlogPage');
+ const pathname = useLocalePathname();
+ const currentUser = useCurrentUser();
+ const { data: paymentData, isLoading: isLoadingPayment } = useCurrentPlan(
+ currentUser?.id
+ );
+
// For non-premium articles, show content immediately with no extra processing
if (!isPremium) {
return (
@@ -40,9 +48,11 @@ export function PremiumGuard({
);
}
- const t = useTranslations('BlogPage');
- const pathname = useLocalePathname();
-
+ // Determine if user has premium access
+ const hasPremiumAccess =
+ paymentData?.currentPlan &&
+ (!paymentData.currentPlan.isFree || paymentData.currentPlan.isLifetime);
+
// If server-side check has already determined access, use that
if (canAccess !== undefined) {
// Server has determined the user has access
@@ -55,9 +65,8 @@ export function PremiumGuard({
);
}
-
+
// Server determined no access, show appropriate message
- const currentUser = useCurrentUser();
if (!currentUser) {
return (
@@ -65,7 +74,7 @@ export function PremiumGuard({
{/* Show partial content before protection */}
{children}
-
+
{/* Enhanced login prompt for server-side blocked content */}
@@ -97,17 +106,6 @@ export function PremiumGuard({
}
}
- // Fallback to client-side check when server-side check is not available
- const currentUser = useCurrentUser();
- const { data: paymentData, isLoading: isLoadingPayment } = useCurrentPlan(
- currentUser?.id
- );
-
- // Determine if user has premium access
- const hasPremiumAccess =
- paymentData?.currentPlan &&
- (!paymentData.currentPlan.isFree || paymentData.currentPlan.isLifetime);
-
// If user is not logged in
if (!currentUser) {
return (
diff --git a/src/lib/premium-access.ts b/src/lib/premium-access.ts
new file mode 100644
index 0000000..6eec9a9
--- /dev/null
+++ b/src/lib/premium-access.ts
@@ -0,0 +1,89 @@
+import { getDb } from '@/db';
+import { payment } from '@/db/schema';
+import { findPlanByPriceId, getAllPricePlans } from '@/lib/price-plan';
+import { PaymentTypes } from '@/payment/types';
+import { and, eq, gt, isNull, or } from 'drizzle-orm';
+
+/**
+ * Check premium access for a specific user ID
+ *
+ * This function combines the logic from getLifetimeStatusAction and getActiveSubscriptionAction
+ * but optimizes it for a single database query to check premium access.
+ */
+export async function checkPremiumAccess(userId: string): Promise {
+ try {
+ const db = await getDb();
+
+ // Get lifetime plan IDs for efficient checking
+ const plans = getAllPricePlans();
+ const lifetimePlanIds = plans
+ .filter((plan) => plan.isLifetime)
+ .map((plan) => plan.id);
+
+ // Single optimized query to check both lifetime and active subscriptions
+ const result = await db
+ .select({
+ id: payment.id,
+ priceId: payment.priceId,
+ type: payment.type,
+ status: payment.status,
+ periodEnd: payment.periodEnd,
+ cancelAtPeriodEnd: payment.cancelAtPeriodEnd,
+ })
+ .from(payment)
+ .where(
+ and(
+ eq(payment.userId, userId),
+ or(
+ // Check for completed lifetime payments
+ and(
+ eq(payment.type, PaymentTypes.ONE_TIME),
+ eq(payment.status, 'completed')
+ ),
+ // Check for active subscriptions that haven't expired
+ and(
+ eq(payment.type, PaymentTypes.SUBSCRIPTION),
+ eq(payment.status, 'active'),
+ or(
+ // Either period hasn't ended yet
+ gt(payment.periodEnd, new Date()),
+ // Or period end is null (ongoing subscription)
+ isNull(payment.periodEnd)
+ )
+ )
+ )
+ )
+ );
+
+ if (!result || result.length === 0) {
+ return false;
+ }
+
+ // Check if any payment grants premium access
+ return result.some((p) => {
+ // For one-time payments, check if it's a lifetime plan
+ if (p.type === PaymentTypes.ONE_TIME && p.status === 'completed') {
+ const plan = findPlanByPriceId(p.priceId);
+ return plan && lifetimePlanIds.includes(plan.id);
+ }
+
+ // For subscriptions, check if they're active and not expired
+ if (p.type === PaymentTypes.SUBSCRIPTION && p.status === 'active') {
+ // If periodEnd is null, it's an ongoing subscription
+ if (!p.periodEnd) {
+ return true;
+ }
+
+ // Check if the subscription period hasn't ended yet
+ const now = new Date();
+ const periodEnd = new Date(p.periodEnd);
+ return periodEnd > now;
+ }
+
+ return false;
+ });
+ } catch (error) {
+ console.error('Error checking premium access for user:', error);
+ return false;
+ }
+}