diff --git a/app/api/analyze-study/route.ts b/app/api/analyze-study/route.ts deleted file mode 100644 index ba717a0..0000000 --- a/app/api/analyze-study/route.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { executeQuerySingle } from "@/lib/db"; -import { validateOrigin } from "@/lib/origin-validator"; - -// This endpoint only returns study metadata - NO user genetic data is processed here -export async function POST(request: NextRequest) { - // Validate origin - const originError = validateOrigin(request); - if (originError) return originError; - - try { - const { studyId } = await request.json(); - - if (!studyId) { - return NextResponse.json({ - success: false, - error: 'Missing study ID' - }, { status: 400 }); - } - - // Get study metadata from database (contains no user data) - const query = ` - SELECT - snps, - strongest_snp_risk_allele, - or_or_beta, - ci_text, - study_accession, - disease_trait - FROM gwas_catalog - WHERE id = ? - AND snps IS NOT NULL AND snps != '' - AND strongest_snp_risk_allele IS NOT NULL AND strongest_snp_risk_allele != '' - AND or_or_beta IS NOT NULL AND or_or_beta != '' - `; - - const study = await executeQuerySingle<{ - snps: string | null; - strongest_snp_risk_allele: string | null; - or_or_beta: string | null; - ci_text: string | null; - study_accession: string | null; - disease_trait: string | null; - }>(query, [studyId]); - - if (!study) { - return NextResponse.json({ - success: false, - error: 'Study not found or missing required data' - }, { status: 404 }); - } - - // Determine effect type from ci_text - // Beta coefficients have "increase" or "decrease" in CI text - // e.g., "[NR] unit increase", "[0.0068-0.0139] unit increase", "[112.27-112.33] increase" - // Odds ratios are just numbers: e.g., "[1.08-1.15]" - const ciTextLower = study.ci_text?.toLowerCase() ?? ''; - const hasIncrease = ciTextLower.includes('increase'); - const hasDecrease = ciTextLower.includes('decrease'); - const isBeta = hasIncrease || hasDecrease; - const effectType = isBeta ? 'beta' : 'OR'; - - // CRITICAL FIX: GWAS Catalog stores ALL beta values as positive numbers - // Direction is encoded in ci_text ("increase" vs "decrease") - // We must negate the value for "decrease" studies before sending to client - let effectSize = study.or_or_beta; - if (isBeta && hasDecrease && !hasIncrease) { - const numericValue = parseFloat(study.or_or_beta || '0'); - effectSize = (-Math.abs(numericValue)).toString(); - } - - // Return only study metadata - client will perform the analysis - return NextResponse.json({ - success: true, - study: { - snps: study.snps, - riskAllele: study.strongest_snp_risk_allele, - effectSize: effectSize, - effectType: effectType, - confidenceInterval: study.ci_text, - gwasId: study.study_accession, - } - }); - - } catch (error) { - console.error('Study analysis error:', error); - return NextResponse.json({ - success: false, - error: 'Internal server error during analysis' - }, { status: 500 }); - } -} diff --git a/app/api/stripe/attach-payment-method/route.ts b/app/api/stripe/attach-payment-method/route.ts new file mode 100644 index 0000000..cedfaa1 --- /dev/null +++ b/app/api/stripe/attach-payment-method/route.ts @@ -0,0 +1,75 @@ +import { NextRequest, NextResponse } from 'next/server'; +import Stripe from 'stripe'; + +const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || 'sk_test_placeholder', { + apiVersion: '2025-02-24.acacia', +}); + +export async function POST(request: NextRequest) { + try { + const { subscriptionId, customerId, paymentMethodId } = await request.json(); + + // Validate inputs + if (!subscriptionId || !customerId || !paymentMethodId) { + return NextResponse.json( + { error: 'Subscription ID, customer ID, and payment method ID required' }, + { status: 400 } + ); + } + + console.log(`[Stripe] Attaching payment method ${paymentMethodId} to customer ${customerId} and subscription ${subscriptionId}`); + + // Attach payment method to customer + await stripe.paymentMethods.attach(paymentMethodId, { + customer: customerId, + }); + + console.log(`[Stripe] Payment method attached to customer`); + + // Set as default payment method for the customer + await stripe.customers.update(customerId, { + invoice_settings: { + default_payment_method: paymentMethodId, + }, + }); + + console.log(`[Stripe] Set as default payment method for customer`); + + // Retrieve the subscription to check for existing discounts + const subscription = await stripe.subscriptions.retrieve(subscriptionId); + console.log(`[Stripe] Retrieved subscription, has ${subscription.discounts?.length || 0} discounts`); + + // Update the subscription to use this payment method while preserving discounts + const updateParams: any = { + default_payment_method: paymentMethodId, + }; + + // Preserve existing discounts if any + if (subscription.discounts && subscription.discounts.length > 0) { + updateParams.discounts = subscription.discounts.map((d: any) => ({ + coupon: d.coupon?.id, + promotion_code: d.promotion_code, + })).filter((d: any) => d.coupon || d.promotion_code); + console.log(`[Stripe] Preserving ${updateParams.discounts.length} discounts during update`); + } + + await stripe.subscriptions.update(subscriptionId, updateParams); + + console.log(`[Stripe] Updated subscription ${subscriptionId} with payment method`); + + return NextResponse.json({ + success: true, + message: 'Payment method attached successfully', + }); + } catch (error: any) { + console.error('Stripe attach payment method error:', error); + + return NextResponse.json( + { + error: 'Failed to attach payment method', + details: error.message || 'Unknown error', + }, + { status: 500 } + ); + } +} diff --git a/app/api/stripe/create-checkout/route.ts b/app/api/stripe/create-checkout/route.ts index 331b9f2..9bcccc5 100644 --- a/app/api/stripe/create-checkout/route.ts +++ b/app/api/stripe/create-checkout/route.ts @@ -9,7 +9,7 @@ const DAYS_PER_MONTH = 30; export async function POST(request: NextRequest) { try { - const { walletAddress } = await request.json(); + const { walletAddress, couponCode } = await request.json(); // Validate inputs if (!walletAddress) { @@ -45,6 +45,49 @@ export async function POST(request: NextRequest) { // Get the base URL for redirect const origin = request.headers.get('origin') || process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'; + // Validate promotion code if provided + let discounts = undefined; + if (couponCode && couponCode.trim()) { + try { + console.log(`[Stripe] Validating promotion code: ${couponCode.trim()}`); + // Search for the promotion code + const promotionCodes = await stripe.promotionCodes.list({ + code: couponCode.trim(), + limit: 1, + }); + + if (promotionCodes.data.length === 0) { + console.log(`[Stripe] Promotion code not found`); + return NextResponse.json( + { error: 'Invalid promotion code' }, + { status: 400 } + ); + } + + const promotionCode = promotionCodes.data[0]; + console.log(`[Stripe] Promotion code retrieved:`, promotionCode.id); + + if (!promotionCode.active) { + console.log(`[Stripe] Promotion code is not active`); + return NextResponse.json( + { error: 'This promotion code is no longer active' }, + { status: 400 } + ); + } + + // Apply the promotion code + discounts = [{ promotion_code: promotionCode.id }]; + console.log(`[Stripe] Promotion code ${promotionCode.id} will be applied to checkout`); + } catch (error: any) { + // Promotion code lookup failed + console.error(`[Stripe] Error validating promotion code:`, error.message); + return NextResponse.json( + { error: `Invalid promotion code: ${error.message || 'Not found'}` }, + { status: 400 } + ); + } + } + // Create Stripe Checkout Session for subscription const session = await stripe.checkout.sessions.create({ payment_method_types: ['card'], @@ -55,6 +98,7 @@ export async function POST(request: NextRequest) { quantity: 1, }, ], + discounts, success_url: `${origin}/payment/success?session_id={CHECKOUT_SESSION_ID}`, cancel_url: `${origin}/payment/cancel`, metadata: { diff --git a/app/api/stripe/create-subscription/route.ts b/app/api/stripe/create-subscription/route.ts index 7b5340f..1b85f46 100644 --- a/app/api/stripe/create-subscription/route.ts +++ b/app/api/stripe/create-subscription/route.ts @@ -7,7 +7,7 @@ const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || 'sk_test_placeholder' export async function POST(request: NextRequest) { try { - const { walletAddress } = await request.json(); + const { walletAddress, couponCode } = await request.json(); // Validate inputs if (!walletAddress) { @@ -50,8 +50,51 @@ export async function POST(request: NextRequest) { }, }); + // Validate promotion code if provided + let promotion_code_id = undefined; + if (couponCode && couponCode.trim()) { + try { + console.log(`[Stripe] Validating promotion code: ${couponCode.trim()}`); + // Search for the promotion code + const promotionCodes = await stripe.promotionCodes.list({ + code: couponCode.trim(), + limit: 1, + }); + + if (promotionCodes.data.length === 0) { + console.log(`[Stripe] Promotion code not found`); + return NextResponse.json( + { error: 'Invalid promotion code' }, + { status: 400 } + ); + } + + const promotionCode = promotionCodes.data[0]; + console.log(`[Stripe] Promotion code retrieved:`, promotionCode.id); + + if (!promotionCode.active) { + console.log(`[Stripe] Promotion code is not active`); + return NextResponse.json( + { error: 'This promotion code is no longer active' }, + { status: 400 } + ); + } + + // Apply the promotion code + promotion_code_id = promotionCode.id; + console.log(`[Stripe] Promotion code ${promotion_code_id} will be applied to subscription`); + } catch (error: any) { + // Promotion code lookup failed + console.error(`[Stripe] Error validating promotion code:`, error.message); + return NextResponse.json( + { error: `Invalid promotion code: ${error.message || 'Not found'}` }, + { status: 400 } + ); + } + } + // Create subscription - const subscription = await stripe.subscriptions.create({ + const subscriptionParams: any = { customer: customer.id, items: [ { @@ -61,22 +104,101 @@ export async function POST(request: NextRequest) { payment_behavior: 'default_incomplete', payment_settings: { save_default_payment_method: 'on_subscription', + payment_method_types: ['card'], }, expand: ['latest_invoice.payment_intent'], metadata: { walletAddress: walletAddress.toLowerCase(), }, - }); + // Always require payment method collection, even if first invoice is $0 + // This ensures we can charge after promotional period ends + collection_method: 'charge_automatically', + }; - const invoice = subscription.latest_invoice as Stripe.Invoice; - const paymentIntent = invoice.payment_intent as Stripe.PaymentIntent; + // Add promotion code if provided + if (promotion_code_id) { + // For subscriptions, use discounts array with promotion_code + subscriptionParams.discounts = [{ promotion_code: promotion_code_id }]; + console.log(`[Stripe] Adding promotion code ${promotion_code_id} to subscription via discounts array`); + } + + const subscription = await stripe.subscriptions.create(subscriptionParams); console.log(`[Stripe] Created subscription: ${subscription.id} for wallet ${walletAddress}`); + const invoice = subscription.latest_invoice as Stripe.Invoice; + + if (!invoice) { + console.error(`[Stripe] No invoice found for subscription ${subscription.id}`); + return NextResponse.json( + { error: 'Failed to create invoice' }, + { status: 500 } + ); + } + + console.log(`[Stripe] Invoice ID: ${invoice.id}, Status: ${invoice.status}, Amount: ${invoice.amount_due}`); + + // Get discount information if available + let discountInfo = null; + if (invoice.discount || (invoice.total_discount_amounts && invoice.total_discount_amounts.length > 0)) { + const discountAmount = invoice.total_discount_amounts?.[0]?.amount || 0; + const originalAmount = invoice.subtotal || 0; + const finalAmount = invoice.amount_due || 0; + + discountInfo = { + originalAmount: (originalAmount / 100).toFixed(2), // Convert cents to dollars + discountAmount: (discountAmount / 100).toFixed(2), + finalAmount: (finalAmount / 100).toFixed(2), + promotionCode: invoice.discount?.promotion_code || null, + }; + + console.log(`[Stripe] Discount applied:`, discountInfo); + } + + const paymentIntent = invoice.payment_intent as Stripe.PaymentIntent | null; + + // If there's no payment intent (e.g., $0 invoice due to 100% discount) + // We need to create a SetupIntent to collect payment method for future charges + if (!paymentIntent) { + console.log(`[Stripe] No payment intent for $0 invoice - creating SetupIntent to collect payment method`); + + const setupIntent = await stripe.setupIntents.create({ + customer: customer.id, + payment_method_types: ['card'], + metadata: { + subscription_id: subscription.id, + wallet_address: walletAddress.toLowerCase(), + }, + }); + + console.log(`[Stripe] SetupIntent created: ${setupIntent.id}`); + + return NextResponse.json({ + success: true, + subscriptionId: subscription.id, + customerId: customer.id, + clientSecret: setupIntent.client_secret, + isSetupIntent: true, // Flag to indicate this is setup, not payment + discount: discountInfo, + }); + } + + if (!paymentIntent.client_secret) { + console.error(`[Stripe] Payment intent exists but has no client_secret:`, paymentIntent.id); + return NextResponse.json( + { error: 'Failed to get payment client secret' }, + { status: 500 } + ); + } + + console.log(`[Stripe] Payment intent: ${paymentIntent.id}, Status: ${paymentIntent.status}`); + return NextResponse.json({ success: true, subscriptionId: subscription.id, clientSecret: paymentIntent.client_secret, + isSetupIntent: false, + discount: discountInfo, }); } catch (error: any) { console.error('Stripe subscription creation error:', error); diff --git a/app/api/studies/route.ts b/app/api/studies/route.ts index 276e9e6..c97b396 100644 --- a/app/api/studies/route.ts +++ b/app/api/studies/route.ts @@ -33,6 +33,7 @@ type RawStudy = { p_value: string | null; pvalue_mlog: string | null; or_or_beta: string | null; + ci_text: string | null; risk_allele_frequency: string | null; strongest_snp_risk_allele: string | null; snps: string | null; @@ -420,6 +421,7 @@ export async function GET(request: NextRequest) { gc.p_value, gc.pvalue_mlog, gc.or_or_beta, + gc.ci_text, gc.risk_allele_frequency, gc.strongest_snp_risk_allele, gc.snps${similarityColumn} @@ -544,6 +546,7 @@ export async function GET(request: NextRequest) { gc.p_value, gc.pvalue_mlog, gc.or_or_beta, + gc.ci_text, gc.risk_allele_frequency, gc.strongest_snp_risk_allele, gc.snps diff --git a/app/components/AuthProvider.tsx b/app/components/AuthProvider.tsx index 2eb51fb..ec756a4 100644 --- a/app/components/AuthProvider.tsx +++ b/app/components/AuthProvider.tsx @@ -138,10 +138,13 @@ export function AuthProvider({ children }: { children: ReactNode }) { daysRemaining: subData.daysRemaining, }); + return subData.isActive; // Return whether subscription is active + } catch (error) { console.error('[AuthProvider] Failed to check subscription:', error); setHasActiveSubscription(false); setSubscriptionData(null); + return false; } finally { setCheckingSubscription(false); } @@ -150,7 +153,27 @@ export function AuthProvider({ children }: { children: ReactNode }) { const refreshSubscription = async () => { const walletAddress = user?.verifiedCredentials?.[0]?.address; if (walletAddress) { - await checkSubscription(walletAddress); + // Clear any cached subscription data first + localStorage.removeItem(`subscription_${walletAddress.toLowerCase()}`); + + // Check immediately + const isActive = await checkSubscription(walletAddress); + + // If still not active, retry up to 3 times with increasing delays + if (!isActive) { + console.log('[AuthProvider] Subscription not active yet, will retry...'); + + const retryDelays = [1000, 2000, 3000]; // 1s, 2s, 3s + for (const delay of retryDelays) { + await new Promise(resolve => setTimeout(resolve, delay)); + console.log(`[AuthProvider] Retry after ${delay}ms...`); + const isNowActive = await checkSubscription(walletAddress); + if (isNowActive) { + console.log('[AuthProvider] ✅ Subscription is now active!'); + break; + } + } + } } }; diff --git a/app/components/PaymentModal.tsx b/app/components/PaymentModal.tsx index ba290ad..4150014 100644 --- a/app/components/PaymentModal.tsx +++ b/app/components/PaymentModal.tsx @@ -281,11 +281,13 @@ export default function PaymentModal({ isOpen, onClose, onSuccess }: PaymentModa const durationDays = Math.round((parseFloat(amount || '0') / 4.99) * 30); trackSubscribedWithCreditCard(durationDays); - // Close modal after 3 seconds and trigger success callback + // Trigger success callback immediately to refresh subscription + onSuccess(); + + // Close modal after 2 seconds setTimeout(() => { onClose(); - onSuccess(); - }, 3000); + }, 2000); }; const handleSendPayment = async () => { @@ -447,23 +449,23 @@ export default function PaymentModal({ isOpen, onClose, onSuccess }: PaymentModa
$4.99/month • Recurring subscription via Stripe
-- Wallet: {primaryWallet.address.slice(0, 6)}...{primaryWallet.address.slice(-4)} -
-