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
2 changes: 1 addition & 1 deletion app/api/traits/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
31 changes: 29 additions & 2 deletions app/components/AuthProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -23,6 +24,7 @@ interface AuthContextType {
refreshSubscription: () => Promise<void>;
initializeDynamic: () => void;
isDynamicInitialized: boolean;
openAuthModal: () => void;
}

const AuthContext = createContext<AuthContextType>({
Expand All @@ -34,6 +36,7 @@ const AuthContext = createContext<AuthContextType>({
refreshSubscription: async () => {},
initializeDynamic: () => {},
isDynamicInitialized: false,
openAuthModal: () => {},
});

export const useAuth = () => useContext(AuthContext);
Expand All @@ -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:', {
Expand All @@ -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;
}

Expand All @@ -70,6 +82,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
const [subscriptionData, setSubscriptionData] = useState<SubscriptionData | null>(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;
Expand Down Expand Up @@ -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 (
Expand All @@ -188,6 +211,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
refreshSubscription: async () => {},
initializeDynamic: () => {},
isDynamicInitialized: false,
openAuthModal: () => {},
}}
>
{children}
Expand All @@ -208,6 +232,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
refreshSubscription,
initializeDynamic,
isDynamicInitialized,
openAuthModal: () => {},
}}
>
{children}
Expand All @@ -219,7 +244,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
<DynamicContextProvider
settings={{
environmentId: environmentId,
walletConnectors: [EthereumWalletConnectors],
walletConnectors: [EthereumWalletConnectors, ZeroDevSmartWalletConnectors],
events: {
onLogout: () => {
setIsAuthenticated(false);
Expand All @@ -233,6 +258,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
>
<AuthStateSync
onAuthStateChange={handleAuthStateChange}
onOpenAuthModal={handleOpenAuthModal}
/>
<AuthContext.Provider
value={{
Expand All @@ -244,6 +270,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
refreshSubscription,
initializeDynamic,
isDynamicInitialized,
openAuthModal,
}}
>
{children}
Expand Down
2 changes: 1 addition & 1 deletion app/components/LLMChatInline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
7 changes: 5 additions & 2 deletions app/components/LLMConfigModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export default function LLMConfigModal({ isOpen, onClose, onSave }: LLMConfigMod
setConfig(newConfig);
}}
>
<option value="nilai">Nillion nilAI (Default)</option>
<option value="nilai">nilAI (Default)</option>
<option value="ollama">Ollama (Local)</option>
<option value="huggingface">HuggingFace</option>
</select>
Expand Down Expand Up @@ -120,6 +120,9 @@ export default function LLMConfigModal({ isOpen, onClose, onSave }: LLMConfigMod
<>
<option value="gpt-oss-20b">gpt-oss-20b</option>
<option value="openai/gpt-oss-120b">openai/gpt-oss-120b</option>
<option value="deepseek-ai/DeepSeek-V3.2">deepseek-ai/DeepSeek-V3.2</option>
<option value="moonshotai/Kimi-K2-Thinking">moonshotai/Kimi-K2-Thinking</option>
<option value="zai-org/GLM-4.6">zai-org/GLM-4.6</option>
<option value="custom">Custom...</option>
</>
)}
Expand Down Expand Up @@ -219,7 +222,7 @@ export default function LLMConfigModal({ isOpen, onClose, onSave }: LLMConfigMod
<div className="privacy-details">
<p><strong>How it works:</strong></p>
<ul>
<li><strong>Nillion nilAI (Default):</strong> 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.</li>
<li><strong>nilAI (Default):</strong> 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.</li>
<li><strong>Ollama (Advanced):</strong> Everything runs locally on your computer. No data leaves your device. <a href="https://ollama.com" target="_blank" rel="noopener noreferrer" className="inline-link">Download Ollama</a> and note that you need a powerful GPU (8GB+ VRAM recommended) for acceptable performance.</li>
<li><strong>HuggingFace:</strong> 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.</li>
</ul>
Expand Down
75 changes: 59 additions & 16 deletions app/components/PaymentModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>('');
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';
Expand Down Expand Up @@ -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
}
}
};
Expand Down Expand Up @@ -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);
Expand All @@ -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)
Expand All @@ -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 = () => {
Expand Down Expand Up @@ -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!);
Expand Down Expand Up @@ -741,8 +779,13 @@ export default function PaymentModal({ isOpen, onClose, onSuccess }: PaymentModa
)}

<p className="processing-note">
Checking subscription status... (attempt {retryCount} of 18)
Checking subscription status... (attempt {retryCount} of 12)
</p>
{successfulChecks > 0 && (
<p className="processing-note" style={{ fontSize: '0.875rem', marginTop: '0.5rem', color: '#3b82f6' }}>
✓ Blockchain indexer processing transaction ({successfulChecks} {successfulChecks === 1 ? 'check' : 'checks'} completed)
</p>
)}
<p className="processing-note" style={{ fontSize: '0.75rem', marginTop: '0.5rem' }}>
Usually takes 1-2 minutes. Do not close this window.
</p>
Expand Down
6 changes: 3 additions & 3 deletions app/components/PremiumPaywall.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
<html lang="en">
<html lang="en" suppressHydrationWarning>
<head>
{/* Theme Script - Must run before any rendering to prevent flash */}
<script
Expand Down
Loading
Loading