From 25e4340eb2444210759a708528df65d247c65fbd Mon Sep 17 00:00:00 2001 From: Vishakh Date: Tue, 9 Dec 2025 15:35:12 -0500 Subject: [PATCH 1/2] Fixed promo code functionality. --- app/components/AuthProvider.tsx | 19 ++++++ app/components/LLMChatInline.tsx | 13 +--- app/components/OverviewReportModal.tsx | 3 +- app/components/PaymentModal.tsx | 3 +- app/components/PremiumPaywall.tsx | 37 ++++------- app/page.tsx | 17 ++++++ lib/analytics.ts | 2 +- lib/prime-verification.ts | 2 +- lib/promo-access.ts | 85 ++++++++++++++++++++++++++ 9 files changed, 142 insertions(+), 39 deletions(-) create mode 100644 lib/promo-access.ts diff --git a/app/components/AuthProvider.tsx b/app/components/AuthProvider.tsx index 138a98d..2eb51fb 100644 --- a/app/components/AuthProvider.tsx +++ b/app/components/AuthProvider.tsx @@ -5,6 +5,7 @@ import { EthereumWalletConnectors } from '@dynamic-labs/ethereum'; import { ZeroDevSmartWalletConnectors } from '@dynamic-labs/ethereum-aa'; import { createContext, useContext, useEffect, useState, useCallback, ReactNode } from 'react'; import { trackUserLoggedIn } from '@/lib/analytics'; +import { hasValidPromoAccess } from '@/lib/promo-access'; interface SubscriptionData { isActive: boolean; @@ -93,6 +94,24 @@ export function AuthProvider({ children }: { children: ReactNode }) { console.log('[AuthProvider] Checking subscription for:', walletAddress); setCheckingSubscription(true); + // OPTIMIZATION: Check promo code first (client-side, instant) + if (hasValidPromoAccess()) { + console.log('[AuthProvider] ✅ Valid promo code found - skipping API call'); + setHasActiveSubscription(true); + setSubscriptionData({ + isActive: true, + expiresAt: null, + daysRemaining: 999, // Promo access (no expiration currently) + totalDaysPurchased: 0, + totalPaid: 0, + paymentCount: 0, + }); + setCheckingSubscription(false); + return; + } + + console.log('[AuthProvider] No promo code, checking Stripe/blockchain...'); + // Query API directly - no caching const response = await fetch('/api/check-subscription', { method: 'POST', diff --git a/app/components/LLMChatInline.tsx b/app/components/LLMChatInline.tsx index 934b1d6..cc4023c 100644 --- a/app/components/LLMChatInline.tsx +++ b/app/components/LLMChatInline.tsx @@ -12,6 +12,7 @@ import { callLLM, callLLMStream, getLLMDescription } from "@/lib/llm-client"; import { getLLMConfig } from "@/lib/llm-config"; import { RobotIcon } from "./Icons"; import { trackLLMQuestionAsked } from "@/lib/analytics"; +import { hasValidPromoAccess } from "@/lib/promo-access"; type AttachmentType = 'text' | 'pdf' | 'csv' | 'tsv'; @@ -83,16 +84,8 @@ export default function AIChatInline() { setMounted(true); // Check for promo code access - const promoStored = localStorage.getItem('promo_access'); - if (promoStored) { - try { - const data = JSON.parse(promoStored); - if (data.code) { - setHasPromoAccess(true); - } - } catch (err) { - // Invalid promo data - } + if (hasValidPromoAccess()) { + setHasPromoAccess(true); } // Check consent diff --git a/app/components/OverviewReportModal.tsx b/app/components/OverviewReportModal.tsx index 46afeb6..1137184 100644 --- a/app/components/OverviewReportModal.tsx +++ b/app/components/OverviewReportModal.tsx @@ -7,6 +7,7 @@ import { useAuth } from "./AuthProvider"; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import { trackOverviewReportGenerated } from "@/lib/analytics"; +import { hasValidPromoAccess } from "@/lib/promo-access"; type GenerationPhase = 'idle' | 'map' | 'reduce' | 'complete' | 'error'; @@ -516,7 +517,7 @@ export default function OverviewReportModal({ isOpen, onClose }: OverviewReportM if (!isOpen) return null; // Check subscription - const hasPromoAccess = typeof window !== 'undefined' && localStorage.getItem('promo_access'); + const hasPromoAccess = hasValidPromoAccess(); const isBlocked = !hasActiveSubscription && !hasPromoAccess; return ( diff --git a/app/components/PaymentModal.tsx b/app/components/PaymentModal.tsx index 48fd57c..ba290ad 100644 --- a/app/components/PaymentModal.tsx +++ b/app/components/PaymentModal.tsx @@ -5,6 +5,7 @@ import { useDynamicContext } from '@dynamic-labs/sdk-react-core'; import { parseUnits, encodeFunctionData, createPublicClient, http, formatUnits } from 'viem'; import { mainnet, base, arbitrum, optimism, polygon, sepolia } from 'viem/chains'; import { verifyPromoCode } from '@/lib/prime-verification'; +import { setPromoAccess } from '@/lib/promo-access'; import StripeSubscriptionForm from './StripeSubscriptionForm'; import { trackSubscribedWithPromoCode, trackSubscribedWithCreditCard, trackSubscribedWithStablecoin } from '@/lib/analytics'; @@ -195,7 +196,7 @@ export default function PaymentModal({ isOpen, onClose, onSuccess }: PaymentModa if (result.valid && result.discount === 0) { // Free access granted! - localStorage.setItem('promo_access', JSON.stringify({ code: promoCode, granted: Date.now() })); + setPromoAccess(promoCode); setPromoMessage({ type: 'success', text: result.message }); // Track promo code subscription diff --git a/app/components/PremiumPaywall.tsx b/app/components/PremiumPaywall.tsx index 6f1e650..94c9c13 100644 --- a/app/components/PremiumPaywall.tsx +++ b/app/components/PremiumPaywall.tsx @@ -3,8 +3,8 @@ import { useState, useEffect } from 'react'; import dynamic from 'next/dynamic'; import { useAuth } from './AuthProvider'; -import { verifyPromoCode } from '@/lib/prime-verification'; import { trackPremiumSectionViewed } from '@/lib/analytics'; +import { hasValidPromoAccess, getPromoCode, clearPromoAccess } from '@/lib/promo-access'; // Lazy load PaymentModal to reduce initial bundle size (viem + @dynamic-labs = ~86MB) const PaymentModal = dynamic(() => import('./PaymentModal'), { @@ -24,21 +24,11 @@ export function PremiumPaywall({ children }: PremiumPaywallProps) { // Check localStorage for existing promo access on mount useEffect(() => { - const stored = localStorage.getItem('promo_access'); - if (stored) { - try { - const data = JSON.parse(stored); - // Re-verify the code (in case logic changed) - const result = verifyPromoCode(data.code); - if (result.valid && result.discount === 0) { - setHasPromoAccess(true); - setPromoCode(data.code); - } else { - // Code no longer valid, clear storage - localStorage.removeItem('promo_access'); - } - } catch (err) { - localStorage.removeItem('promo_access'); + if (hasValidPromoAccess()) { + setHasPromoAccess(true); + const code = getPromoCode(); + if (code) { + setPromoCode(code); } } }, []); @@ -55,21 +45,18 @@ export function PremiumPaywall({ children }: PremiumPaywallProps) { }, []); const handleRemovePromoCode = () => { - localStorage.removeItem('promo_access'); + clearPromoAccess(); setHasPromoAccess(false); setPromoCode(''); }; const handleModalSuccess = async () => { // Refresh promo access state - const stored = localStorage.getItem('promo_access'); - if (stored) { - try { - const data = JSON.parse(stored); - setHasPromoAccess(true); - setPromoCode(data.code); - } catch (err) { - // Ignore + if (hasValidPromoAccess()) { + setHasPromoAccess(true); + const code = getPromoCode(); + if (code) { + setPromoCode(code); } } // Refresh subscription status in AuthProvider to update UI immediately diff --git a/app/page.tsx b/app/page.tsx index 81ce4ab..820e1a2 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -20,6 +20,7 @@ import GuidedTour from "./components/GuidedTour"; import { hasMatchingSNPs } from "@/lib/snp-utils"; import { analyzeStudyClientSide } from "@/lib/risk-calculator"; import { isDevModeEnabled } from "@/lib/dev-mode"; +import { hasValidPromoAccess, clearPromoAccess } from "@/lib/promo-access"; import { trackSearch, trackRunAllStarted, @@ -1244,6 +1245,22 @@ function MainContent() { > Cancel Subscription + {hasValidPromoAccess() && ( + + )} )} diff --git a/lib/analytics.ts b/lib/analytics.ts index b8c21bc..21196cf 100644 --- a/lib/analytics.ts +++ b/lib/analytics.ts @@ -139,7 +139,7 @@ export function trackOverviewReportGenerated(resultCount: number) { */ export function trackSubscribedWithPromoCode(promoCode: string) { trackEvent('subscribed_promo_code', { - promo_code: promoCode, + code_type: 'delicate_prime', }); } diff --git a/lib/prime-verification.ts b/lib/prime-verification.ts index 87538ab..cd9e42e 100644 --- a/lib/prime-verification.ts +++ b/lib/prime-verification.ts @@ -132,7 +132,7 @@ export function verifyPromoCode(code: string): { valid: boolean; discount: numbe return { valid: true, discount: 0, // 0 = free (100% discount) - message: '🎉 Delicate prime verified! Free access granted.' + message: '🎉 Valid promo code! Free access granted.' }; } diff --git a/lib/promo-access.ts b/lib/promo-access.ts new file mode 100644 index 0000000..88f65de --- /dev/null +++ b/lib/promo-access.ts @@ -0,0 +1,85 @@ +/** + * Centralized promo code access utilities + * Ensures consistent validation across all components + */ + +import { verifyPromoCode } from './prime-verification'; + +const PROMO_STORAGE_KEY = 'promo_access'; + +interface PromoAccessData { + code: string; + granted: number; +} + +/** + * Check if user has valid promo access + * Re-verifies the stored code to prevent tampering + */ +export function hasValidPromoAccess(): boolean { + if (typeof window === 'undefined') return false; + + const stored = localStorage.getItem(PROMO_STORAGE_KEY); + if (!stored) return false; + + try { + const data: PromoAccessData = JSON.parse(stored); + + // Must have a code + if (!data.code) { + localStorage.removeItem(PROMO_STORAGE_KEY); + return false; + } + + // Re-verify the code is actually valid + const result = verifyPromoCode(data.code); + + if (!result.valid || result.discount !== 0) { + // Code is no longer valid, remove it + localStorage.removeItem(PROMO_STORAGE_KEY); + return false; + } + + return true; + } catch (err) { + // Invalid JSON or other error, clear storage + localStorage.removeItem(PROMO_STORAGE_KEY); + return false; + } +} + +/** + * Get the stored promo code (if valid) + * Returns null if no valid promo access + */ +export function getPromoCode(): string | null { + if (!hasValidPromoAccess()) return null; + + try { + const stored = localStorage.getItem(PROMO_STORAGE_KEY); + if (!stored) return null; + + const data: PromoAccessData = JSON.parse(stored); + return data.code || null; + } catch { + return null; + } +} + +/** + * Store validated promo access + */ +export function setPromoAccess(code: string): void { + const data: PromoAccessData = { + code, + granted: Date.now(), + }; + localStorage.setItem(PROMO_STORAGE_KEY, JSON.stringify(data)); +} + +/** + * Remove promo access + */ +export function clearPromoAccess(): void { + localStorage.removeItem(PROMO_STORAGE_KEY); +} From 05a9a8e40730da8388b798eed606699c54f29958 Mon Sep 17 00:00:00 2001 From: Vishakh Date: Tue, 9 Dec 2025 15:42:01 -0500 Subject: [PATCH 2/2] Made the guided tour clearer. --- app/globals.css | 4 ++-- app/page.tsx | 11 +++++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/app/globals.css b/app/globals.css index 5736ae9..6fd9aad 100644 --- a/app/globals.css +++ b/app/globals.css @@ -5876,8 +5876,8 @@ details[open] .summary-arrow { left: 0; right: 0; bottom: 0; - background: rgba(0, 0, 0, 0.7); - backdrop-filter: blur(4px); + background: rgba(0, 0, 0, 0.4); + backdrop-filter: blur(1px); z-index: 10000; opacity: 0; transition: opacity 0.3s ease; diff --git a/app/page.tsx b/app/page.tsx index 820e1a2..e75ebb3 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -220,6 +220,9 @@ function MainContent() { // Premium features overview collapsed state const [featuresOverviewCollapsed, setFeaturesOverviewCollapsed] = useState(false); + // Guided tour state (declare early since it's used in useEffect below) + const [showGuidedTour, setShowGuidedTour] = useState(false); + // Load saved tab from localStorage after mount (dev mode only) useEffect(() => { if (isDevModeEnabled()) { @@ -243,7 +246,12 @@ function MainContent() { console.log('[MainContent] Premium tab accessed, initializing Dynamic...'); initializeDynamic(); } - }, [activeTab, isDynamicInitialized, initializeDynamic]); + // Dismiss guided tour when switching to Premium tab + if (activeTab === 'premium' && showGuidedTour) { + setShowGuidedTour(false); + localStorage.setItem('tour_dismissed', 'true'); + } + }, [activeTab, isDynamicInitialized, initializeDynamic, showGuidedTour]); const [filters, setFilters] = useState(defaultFilters); const [debouncedSearch, setDebouncedSearch] = useState(defaultFilters.search); @@ -268,7 +276,6 @@ function MainContent() { const [showOverviewReportModal, setShowOverviewReportModal] = useState(false); const [showSubscriptionMenu, setShowSubscriptionMenu] = useState(false); const [showRunAllDisclaimer, setShowRunAllDisclaimer] = useState(false); - const [showGuidedTour, setShowGuidedTour] = useState(false); const [runAllStatus, setRunAllStatus] = useState<{ phase: 'fetching' | 'downloading' | 'decompressing' | 'parsing' | 'storing' | 'analyzing' | 'embeddings' | 'complete' | 'error'; fetchedBatches: number;