Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions app/components/AuthProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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',
Expand Down
13 changes: 3 additions & 10 deletions app/components/LLMChatInline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion app/components/OverviewReportModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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 (
Expand Down
3 changes: 2 additions & 1 deletion app/components/PaymentModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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
Expand Down
37 changes: 12 additions & 25 deletions app/components/PremiumPaywall.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'), {
Expand All @@ -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);
}
}
}, []);
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
28 changes: 26 additions & 2 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -219,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()) {
Expand All @@ -242,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<Filters>(defaultFilters);
const [debouncedSearch, setDebouncedSearch] = useState<string>(defaultFilters.search);
Expand All @@ -267,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;
Expand Down Expand Up @@ -1244,6 +1252,22 @@ function MainContent() {
>
Cancel Subscription
</button>
{hasValidPromoAccess() && (
<button
onClick={() => {
setShowSubscriptionMenu(false);
if (!confirm('Are you sure you want to remove your promo code? You will lose premium access immediately.')) {
return;
}
clearPromoAccess();
alert('✓ Promo code removed');
window.location.reload();
}}
className="subscription-menu-item cancel"
>
Remove Promo Code
</button>
)}
</div>
</>
)}
Expand Down
2 changes: 1 addition & 1 deletion lib/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
});
}

Expand Down
2 changes: 1 addition & 1 deletion lib/prime-verification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.'
};
}

Expand Down
85 changes: 85 additions & 0 deletions lib/promo-access.ts
Original file line number Diff line number Diff line change
@@ -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);
}
Loading