diff --git a/app/api/traits/route.ts b/app/api/traits/route.ts index adbc225..afbd9f7 100644 --- a/app/api/traits/route.ts +++ b/app/api/traits/route.ts @@ -12,7 +12,7 @@ export async function GET(request: NextRequest) { `SELECT DISTINCT COALESCE(NULLIF(TRIM(mapped_trait), ''), NULLIF(TRIM(disease_trait), '')) AS trait FROM gwas_catalog WHERE COALESCE(NULLIF(TRIM(mapped_trait), ''), NULLIF(TRIM(disease_trait), '')) IS NOT NULL - ORDER BY COALESCE(NULLIF(TRIM(mapped_trait), ''), NULLIF(TRIM(disease_trait), '')) COLLATE NOCASE` + ORDER BY trait` ); const traits = rows diff --git a/app/components/AuthProvider.tsx b/app/components/AuthProvider.tsx index 1602989..138a98d 100644 --- a/app/components/AuthProvider.tsx +++ b/app/components/AuthProvider.tsx @@ -2,6 +2,7 @@ import { DynamicContextProvider, DynamicWidget, useDynamicContext } from '@dynamic-labs/sdk-react-core'; 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'; @@ -23,6 +24,7 @@ interface AuthContextType { refreshSubscription: () => Promise; initializeDynamic: () => void; isDynamicInitialized: boolean; + openAuthModal: () => void; } const AuthContext = createContext({ @@ -34,6 +36,7 @@ const AuthContext = createContext({ refreshSubscription: async () => {}, initializeDynamic: () => {}, isDynamicInitialized: false, + openAuthModal: () => {}, }); export const useAuth = () => useContext(AuthContext); @@ -42,10 +45,12 @@ export const useAuth = () => useContext(AuthContext); // NOTE: No longer automatically checks subscription - must be triggered manually function AuthStateSync({ onAuthStateChange, + onOpenAuthModal, }: { onAuthStateChange: (isAuth: boolean, user: any) => void; + onOpenAuthModal: (openFn: () => void) => void; }) { - const { user: dynamicUser } = useDynamicContext(); + const { user: dynamicUser, setShowAuthFlow } = useDynamicContext(); useEffect(() => { console.log('[AuthStateSync] Dynamic state:', { @@ -60,6 +65,13 @@ function AuthStateSync({ onAuthStateChange(isAuth, dynamicUser); }, [dynamicUser, onAuthStateChange]); + // Provide the openAuthModal function to the parent + useEffect(() => { + if (setShowAuthFlow) { + onOpenAuthModal(() => setShowAuthFlow(true)); + } + }, [setShowAuthFlow, onOpenAuthModal]); + return null; } @@ -70,6 +82,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { const [subscriptionData, setSubscriptionData] = useState(null); const [checkingSubscription, setCheckingSubscription] = useState(false); // Changed default to false const [isDynamicInitialized, setIsDynamicInitialized] = useState(false); + const [openAuthModalFn, setOpenAuthModalFn] = useState<(() => void) | null>(null); // If environment ID is not set, render without Dynamic (useful for CI/CD builds) const environmentId = process.env.NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID; @@ -175,6 +188,16 @@ export function AuthProvider({ children }: { children: ReactNode }) { } }, [isDynamicInitialized, checkSubscription, isAuthenticated]); + const handleOpenAuthModal = useCallback((openFn: () => void) => { + setOpenAuthModalFn(() => openFn); + }, []); + + const openAuthModal = useCallback(() => { + if (openAuthModalFn) { + openAuthModalFn(); + } + }, [openAuthModalFn]); + // If Dynamic is not enabled (no environment ID), render children with default auth context if (!isDynamicEnabled) { return ( @@ -188,6 +211,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { refreshSubscription: async () => {}, initializeDynamic: () => {}, isDynamicInitialized: false, + openAuthModal: () => {}, }} > {children} @@ -208,6 +232,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { refreshSubscription, initializeDynamic, isDynamicInitialized, + openAuthModal: () => {}, }} > {children} @@ -219,7 +244,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { { setIsAuthenticated(false); @@ -233,6 +258,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { > {children} diff --git a/app/components/LLMChatInline.tsx b/app/components/LLMChatInline.tsx index f3bb1c5..934b1d6 100644 --- a/app/components/LLMChatInline.tsx +++ b/app/components/LLMChatInline.tsx @@ -175,7 +175,7 @@ export default function AIChatInline() { message: config.provider === 'nilai' ? 'You\'re using nilAI (privacy-preserving TEE) - your data is maximally protected!' : 'You\'re using Ollama (local processing) - your data never leaves your device!', - tip: 'Want more advanced models? Use the ⚙️ LLM button (top right) to switch to HuggingFace for better performance. Note: HuggingFace requires creating your own account and involves some privacy tradeoffs.', + tip: 'Want more advanced models by going easier on privacy? Use the LLM button (top right) to switch to HuggingFace for more model choices. You will need to create your own HuggingFace account with a subscription.', }; } else if (config.provider === 'huggingface') { return { diff --git a/app/components/LLMConfigModal.tsx b/app/components/LLMConfigModal.tsx index bb67fc4..d234ae3 100644 --- a/app/components/LLMConfigModal.tsx +++ b/app/components/LLMConfigModal.tsx @@ -78,7 +78,7 @@ export default function LLMConfigModal({ isOpen, onClose, onSave }: LLMConfigMod setConfig(newConfig); }} > - + @@ -120,6 +120,9 @@ export default function LLMConfigModal({ isOpen, onClose, onSave }: LLMConfigMod <> + + + )} @@ -219,7 +222,7 @@ export default function LLMConfigModal({ isOpen, onClose, onSave }: LLMConfigMod

How it works:

    -
  • Nillion nilAI (Default): Your genetic variants are sent to a Trusted Execution Environment (TEE) where analysis happens in an encrypted, isolated environment. No one, not even Nillion, can see your raw data. Usage is covered by your Monadic DNA subscription.
  • +
  • nilAI (Default): Your genetic variants are sent to a Trusted Execution Environment (TEE) where analysis happens in an encrypted, isolated environment. No one, not even Nillion, can see your raw data. Usage is covered by your Monadic DNA subscription.
  • Ollama (Advanced): Everything runs locally on your computer. No data leaves your device. Download Ollama and note that you need a powerful GPU (8GB+ VRAM recommended) for acceptable performance.
  • HuggingFace: Your variants are sent directly from your browser to HuggingFace's servers for analysis. Requires your own HuggingFace account and paid subscription. We never see or store your data.
diff --git a/app/components/PaymentModal.tsx b/app/components/PaymentModal.tsx index 84aaef2..48fd57c 100644 --- a/app/components/PaymentModal.tsx +++ b/app/components/PaymentModal.tsx @@ -30,6 +30,7 @@ export default function PaymentModal({ isOpen, onClose, onSuccess }: PaymentModa const [promoMessage, setPromoMessage] = useState<{ type: 'success' | 'error' | ''; text: string }>({ type: '', text: '' }); const [transactionHash, setTransactionHash] = useState(''); const [retryCount, setRetryCount] = useState(0); + const [successfulChecks, setSuccessfulChecks] = useState(0); const paymentWallet = process.env.NEXT_PUBLIC_EVM_PAYMENT_WALLET_ADDRESS || ''; const testnetEnabled = process.env.NEXT_PUBLIC_ENABLE_TESTNET_CHAINS === 'true'; @@ -132,13 +133,36 @@ export default function PaymentModal({ isOpen, onClose, onSuccess }: PaymentModa const detectChain = async () => { if (primaryWallet) { try { - // Type assertion for getWalletClient which exists at runtime but not in types - const walletClient = await (primaryWallet as any).getWalletClient?.(); - if (walletClient && 'chain' in walletClient && walletClient.chain) { - setConnectedChain(walletClient.chain.name); + // Try multiple methods to get chain info (compatibility with smart wallets and regular wallets) + + // Method 1: Try connector.getNetwork() for smart wallets + if ((primaryWallet as any).connector?.getNetwork) { + const network = await (primaryWallet as any).connector.getNetwork(); + if (network?.name) { + setConnectedChain(network.name); + return; + } + } + + // Method 2: Try getWalletClient for regular wallets + if ((primaryWallet as any).getWalletClient) { + const walletClient = await (primaryWallet as any).getWalletClient(); + if (walletClient && 'chain' in walletClient && walletClient.chain) { + setConnectedChain(walletClient.chain.name); + return; + } + } + + // Method 3: Fall back to checking connector chain directly + if ((primaryWallet as any).connector?.chain?.name) { + setConnectedChain((primaryWallet as any).connector.chain.name); + return; } + + console.log('Could not detect chain, will default to user selection'); } catch (err) { console.error('Failed to detect chain:', err); + // Non-critical error - user can still manually select chain } } }; @@ -188,8 +212,9 @@ export default function PaymentModal({ isOpen, onClose, onSuccess }: PaymentModa }; // Poll subscription status after blockchain payment - const pollSubscriptionStatus = async (walletAddress: string, maxAttempts: number = 18) => { + const pollSubscriptionStatus = async (walletAddress: string, maxAttempts: number = 12) => { const POLL_INTERVAL = 10000; // 10 seconds + let consecutiveSuccessfulChecks = 0; for (let attempt = 1; attempt <= maxAttempts; attempt++) { setRetryCount(attempt); @@ -207,18 +232,30 @@ export default function PaymentModal({ isOpen, onClose, onSuccess }: PaymentModa if (response.ok) { const result = await response.json(); - if (result.success && result.subscription.isActive) { - console.log('[PaymentModal] Subscription activated!'); - // Success! Subscription is active - setTimeout(() => { - onSuccess(); - onClose(); - }, 1000); - return; + if (result.success) { + consecutiveSuccessfulChecks++; + setSuccessfulChecks(consecutiveSuccessfulChecks); + + if (result.subscription.isActive) { + console.log('[PaymentModal] ✅ Subscription activated!'); + // Success! Subscription is active + setTimeout(() => { + onSuccess(); + onClose(); + }, 1000); + return; + } else { + console.log(`[PaymentModal] ⏳ API responded successfully but subscription not yet active (check ${consecutiveSuccessfulChecks})`); + console.log('[PaymentModal] This is normal - blockchain indexer needs time to process the transaction'); + } + } else { + console.warn('[PaymentModal] ⚠️ API returned success=false:', result.error); } + } else { + console.error('[PaymentModal] ❌ API request failed with status:', response.status); } } catch (error) { - console.error('[PaymentModal] Error checking subscription:', error); + console.error('[PaymentModal] ❌ Error checking subscription:', error); } // Wait before next attempt (unless it's the last attempt) @@ -228,7 +265,7 @@ export default function PaymentModal({ isOpen, onClose, onSuccess }: PaymentModa } // Max attempts reached - show timeout message - setError('Transaction confirmed but subscription verification is taking longer than expected. Please refresh the page in a few minutes to check your subscription status.'); + setError(`Transaction submitted to blockchain but not yet indexed. This can take a few minutes. You can close this modal and refresh the page in 2-3 minutes to check your subscription status, or view your transaction on the block explorer using the link above.`); }; const handleCardPaymentSuccess = () => { @@ -351,6 +388,7 @@ export default function PaymentModal({ isOpen, onClose, onSuccess }: PaymentModa setTransactionHash(txHash); setStep('confirming'); setRetryCount(0); + setSuccessfulChecks(0); // Start polling for subscription status await pollSubscriptionStatus(primaryWallet.address!); @@ -741,8 +779,13 @@ export default function PaymentModal({ isOpen, onClose, onSuccess }: PaymentModa )}

- Checking subscription status... (attempt {retryCount} of 18) + Checking subscription status... (attempt {retryCount} of 12)

+ {successfulChecks > 0 && ( +

+ ✓ Blockchain indexer processing transaction ({successfulChecks} {successfulChecks === 1 ? 'check' : 'checks'} completed) +

+ )}

Usually takes 1-2 minutes. Do not close this window.

diff --git a/app/components/PremiumPaywall.tsx b/app/components/PremiumPaywall.tsx index 8dfcf96..6f1e650 100644 --- a/app/components/PremiumPaywall.tsx +++ b/app/components/PremiumPaywall.tsx @@ -60,7 +60,7 @@ export function PremiumPaywall({ children }: PremiumPaywallProps) { setPromoCode(''); }; - const handleModalSuccess = () => { + const handleModalSuccess = async () => { // Refresh promo access state const stored = localStorage.getItem('promo_access'); if (stored) { @@ -72,8 +72,8 @@ export function PremiumPaywall({ children }: PremiumPaywallProps) { // Ignore } } - // PaymentModal now handles subscription polling automatically - // No need to refresh here + // Refresh subscription status in AuthProvider to update UI immediately + await refreshSubscription(); }; // Always show content diff --git a/app/layout.tsx b/app/layout.tsx index 2203b9d..1e0d028 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -41,7 +41,7 @@ export default function RootLayout({ children: React.ReactNode; }) { return ( - + {/* Theme Script - Must run before any rendering to prevent flash */}