diff --git a/README.md b/README.md index 4a28c40..5f77a61 100644 --- a/README.md +++ b/README.md @@ -49,15 +49,15 @@ GuardScope/ - **Database/Auth:** Supabase PostgreSQL, Supabase Auth, RLS - **Threat intel:** VirusTotal, Google Safe Browsing, PhishTank, URLhaus, Spamhaus - **Rate limiting:** Upstash Redis -- **Payments:** Stripe and Paystack, currently suspended during the launch-code promo period +- **Payments:** Paystack recurring subscriptions (credential-gated until activation) - **Email:** Resend - **Monitoring:** Sentry -## Quotas And Beta Access +## Access Model -- Anonymous users: 5 analyses per day per IP. -- Signed-in free users: 5 analyses per month per account. -- Pro and Team users: unlimited analyses, subject to abuse rate limits. +- Migration-safe `legacy` mode preserves the currently published extension. +- Trial mode requires an account and provides five lifetime trial scans. +- Pro and Team access is subscription-authoritative, subject to abuse rate limits. - Launch promo codes: 100 seeded `GS-...` codes, each granting 30 days of Pro access after redemption. ## Privacy Model diff --git a/backend/.env.example b/backend/.env.example index 6e7f0fb..65e6384 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -3,6 +3,7 @@ NEXT_PUBLIC_APP_URL=https://guardscope.app NEXT_PUBLIC_SITE_URL=https://guardscope.app NEXT_PUBLIC_EXTENSION_STATUS=early_access NEXT_PUBLIC_CHROME_WEB_STORE_URL= +GUARDSCOPE_ACCESS_MODE=legacy # InceptionLabs Mercury-2 (required) INCEPTION_API_KEY= @@ -28,11 +29,8 @@ RESEND_API_KEY= # Sentry SENTRY_DSN= -# Stripe (payments currently suspended during early access) -STRIPE_SECRET_KEY= -STRIPE_WEBHOOK_SECRET= -STRIPE_PRO_PRICE_ID= - -# Paystack (payments currently suspended during early access) +# Paystack (keep PAYMENTS_ENABLED=false until every live value is configured) +PAYMENTS_ENABLED=false PAYSTACK_SECRET_KEY= -PAYSTACK_PRO_PLAN_CODE= +PAYSTACK_PRO_MONTHLY_PLAN_CODE= +PAYSTACK_PRO_ANNUAL_PLAN_CODE= diff --git a/backend/PAYSTACK_SETUP.md b/backend/PAYSTACK_SETUP.md new file mode 100644 index 0000000..4317642 --- /dev/null +++ b/backend/PAYSTACK_SETUP.md @@ -0,0 +1,23 @@ +# GuardScope Paystack activation + +The subscription code is deploy-safe while billing is disabled. Keep these values in place until the Paystack plans and webhook are ready: + +```env +GUARDSCOPE_ACCESS_MODE=legacy +PAYMENTS_ENABLED=false +PAYSTACK_SECRET_KEY= +PAYSTACK_PRO_MONTHLY_PLAN_CODE= +PAYSTACK_PRO_ANNUAL_PLAN_CODE= +``` + +## Activation order + +1. Apply `supabase/migrations/20260811160000_subscription_foundation.sql`. +2. Create monthly and annual recurring plans in Paystack, both in NGN. +3. Configure the Paystack webhook URL as `https://guardscope.app/api/paystack/webhook`. +4. Add the live secret key and both plan codes to the production environment. +5. Set `PAYMENTS_ENABLED=true` and verify both checkout intervals with a real low-value test plan before launch pricing is finalized. +6. Set `GUARDSCOPE_ACCESS_MODE=trial` to require accounts and enforce the five lifetime trial scans. +7. After checkout, renewal, failed-payment, cancellation, and management-link checks pass, set `GUARDSCOPE_ACCESS_MODE=paid`. + +`legacy` preserves the currently published extension behavior. `trial` and `paid` use the same server-authoritative entitlement record; neither mode trusts a counter stored in the browser extension. diff --git a/backend/app/account/AccountDashboard.tsx b/backend/app/account/AccountDashboard.tsx new file mode 100644 index 0000000..6a862d7 --- /dev/null +++ b/backend/app/account/AccountDashboard.tsx @@ -0,0 +1,126 @@ +'use client' + +import { useEffect, useState } from 'react' + +interface AccountStatus { + email: string | null + accessPlan: 'trial' | 'pro' | 'team' + subscriptionStatus: string + trialScansUsed: number + trialScanLimit: number + trialScansRemaining: number + paymentReady: boolean + currentPeriodEnd: string | null + nextPaymentAt: string | null + cancelAtPeriodEnd: boolean +} + +export function AccountDashboard() { + const [status, setStatus] = useState(null) + const [error, setError] = useState('') + const [billingLoading, setBillingLoading] = useState(false) + + useEffect(() => { + fetch('/api/auth/session', { method: 'POST', cache: 'no-store' }) + .then(() => fetch('/api/account/status', { cache: 'no-store' })) + .then(async (res) => { + if (res.status === 401) { + window.location.href = '/signup' + return null + } + if (!res.ok) throw new Error('Account status is temporarily unavailable.') + return res.json() as Promise + }) + .then((data) => data && setStatus(data)) + .catch((reason: Error) => setError(reason.message)) + }, []) + + async function startCheckout(interval: 'monthly' | 'annual') { + setBillingLoading(true) + setError('') + try { + const res = await fetch('/api/paystack/initialize', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ interval }), + }) + const data = await res.json() as { url?: string; message?: string; error?: string } + if (!res.ok || !data.url) throw new Error(data.message ?? data.error ?? 'Billing is not available yet.') + window.location.href = data.url + } catch (reason) { + setError(reason instanceof Error ? reason.message : 'Billing is not available yet.') + setBillingLoading(false) + } + } + + async function manageBilling() { + setBillingLoading(true) + setError('') + try { + const res = await fetch('/api/paystack/manage', { method: 'POST' }) + const data = await res.json() as { url?: string; message?: string; error?: string } + if (!res.ok || !data.url) throw new Error(data.message ?? data.error ?? 'Unable to open billing management.') + window.location.href = data.url + } catch (reason) { + setError(reason instanceof Error ? reason.message : 'Unable to open billing management.') + setBillingLoading(false) + } + } + + async function signOut() { + await fetch('/api/auth/signout', { method: 'POST' }) + window.location.href = '/' + } + + if (!status && !error) return
Loading your protection status…
+ + return ( +
+ {error &&
{error}
} + {status && ( + <> +
+
+ Protection account +

{status.accessPlan === 'trial' ? 'Your GuardScope trial' : 'GuardScope Pro is active'}

+

{status.email}

+
+ {status.subscriptionStatus.replace('_', ' ')} +
+ +
+
+ Plan{status.accessPlan === 'trial' ? '5-scan trial' : status.accessPlan.toUpperCase()} +
+
+ Trial scans remaining{status.trialScansRemaining} + {status.trialScansUsed} of {status.trialScanLimit} used +
+
+ Next billing date{status.nextPaymentAt ? new Date(status.nextPaymentAt).toLocaleDateString() : '—'} +
+
+ +
+
+ Billing +

{status.paymentReady ? 'Choose your protection plan' : 'Paystack connection pending'}

+

{status.paymentReady ? 'Secure recurring billing managed by Paystack.' : 'Your account and trial are ready. Subscription checkout will unlock automatically when the live Paystack details are connected.'}

+
+
+ {status.accessPlan === 'trial' ? ( + <> + + + + ) : ( + + )} + +
+
+ + )} +
+ ) +} diff --git a/backend/app/account/page.tsx b/backend/app/account/page.tsx new file mode 100644 index 0000000..91c0f17 --- /dev/null +++ b/backend/app/account/page.tsx @@ -0,0 +1,22 @@ +import type { Metadata } from 'next' +import { Navbar } from '../components/Navbar' +import { Footer } from '../components/Footer' +import { AccountDashboard } from './AccountDashboard' + +export const metadata: Metadata = { + title: 'Account', + description: 'Manage your GuardScope trial, protection plan, and billing status.', + robots: { index: false, follow: false }, +} + +export default function AccountPage() { + return ( + <> + +
+ +
+