import { useState, useEffect } from "react"; import { useNavigate, useSearchParams } from "react-router-dom"; import { useAuth } from "../../contexts/AuthContext"; import { createAddress, getAddresses } from "../../api/addresses"; import { createAlert } from "../../api/alerts"; import { updateNotificationConfig, testNotificationChannels, } from "../../api/notificationConfig"; import { createOnboardingCheckout, verifyCheckoutSession, activateFreeTier } from "../../api/stripe"; import Input from "../../components/Input"; import Button from "../../components/Button"; import TierPicker from "../../components/TierPicker"; import "./Subscribe.css"; const TIER_LIMITS = { free: { maxAlertTypes: 1, channels: ["email"] }, premium: { maxAlertTypes: 2, channels: ["email", "discord", "telegram"] }, pro: { maxAlertTypes: 4, channels: ["email", "discord", "telegram", "slack"] }, }; const STEPS = [ "Create Account", "Choose Plan", "Add Wallet", "Alert Rules", "Notifications", "Done", ]; export default function Subscribe() { const { currentUser, signup } = useAuth(); const navigate = useNavigate(); const [searchParams, setSearchParams] = useSearchParams(); const hasPaymentReturn = searchParams.get("payment") === "success"; const isUpgrade = searchParams.get("upgrade") === "true"; const [step, setStep] = useState(hasPaymentReturn || currentUser ? 2 : 1); const [loading, setLoading] = useState(hasPaymentReturn); const [error, setError] = useState(""); const [skipWarning, setSkipWarning] = useState(""); const [testResults, setTestResults] = useState(null); const [testLoading, setTestLoading] = useState(false); const [data, setData] = useState({ selectedTier: "", email: "", password: "", confirmPassword: "", walletAddress: "", walletLabel: "", createdAddressId: null, alertIncomingTx: false, alertOutgoingTx: false, alertLargeTransfer: false, largeTransferThreshold: "", alertBalanceBelow: false, balanceBelowThreshold: "", discordWebhookUrl: "", slackWebhookUrl: "", notificationEmail: "", alertsCreated: [], notificationConfigured: false, }); function set(field, value) { setData((prev) => ({ ...prev, [field]: value })); } const tierLimits = TIER_LIMITS[data.selectedTier] || TIER_LIMITS.free; useEffect(() => { if (!currentUser || isUpgrade) return; getAddresses() .then((addresses) => { if (addresses.length > 0) { navigate("/addresses", { replace: true }); } }) .catch(() => { }); }, [currentUser, navigate, isUpgrade]); useEffect(() => { const payment = searchParams.get("payment"); const sessionId = searchParams.get("session_id"); if (payment === "cancelled") { setSearchParams({}, { replace: true }); setStep(2); setError("Payment was cancelled. Please try again."); return; } if (payment !== "success" || !sessionId) return; // Phase 1: no account yet — create it. This triggers an auth state change // which remounts the component (App.jsx swaps route trees). The URL params // are preserved so Phase 2 runs on the next mount. if (!currentUser) { const savedEmail = sessionStorage.getItem("kp_onboard_email"); const savedPw = sessionStorage.getItem("kp_onboard_pw"); if (!savedEmail || !savedPw) { setSearchParams({}, { replace: true }); setError("Session expired. Please start the signup process again."); setStep(1); setLoading(false); return; } setLoading(true); signup(savedEmail, savedPw).catch((err) => { setSearchParams({}, { replace: true }); setError("Account creation failed: " + err.message); setStep(1); setLoading(false); }); return; } // Phase 2: authenticated — verify the checkout and activate subscription. setSearchParams({}, { replace: true }); setLoading(true); sessionStorage.removeItem("kp_onboard_email"); sessionStorage.removeItem("kp_onboard_pw"); verifyCheckoutSession(sessionId) .then(() => { setStep(3); }) .catch((err) => { setError("Payment verification failed: " + err.message); setStep(2); }) .finally(() => setLoading(false)); }, [currentUser, searchParams, setSearchParams, signup]); // ── Step handlers ───────────────────────────────────────────────────────── function handleStep1() { setError(""); if (!data.email || !data.password || !data.confirmPassword) { setError("Please fill in all fields"); return; } if (data.password !== data.confirmPassword) { setError("Passwords do not match"); return; } if (data.password.length < 6) { setError("Password must be at least 6 characters"); return; } setStep(2); } async function handleStep2() { setError(""); if (!data.selectedTier) { setError("Please select a plan to continue"); return; } try { setLoading(true); if (data.selectedTier === "free") { if (!currentUser) { await signup(data.email, data.password); } await activateFreeTier(); setStep(3); return; } sessionStorage.setItem("kp_onboard_email", data.email); sessionStorage.setItem("kp_onboard_pw", data.password); const { url } = await createOnboardingCheckout(data.email, data.selectedTier); window.location.href = url; } catch (err) { if (err.code === "auth/email-already-in-use") { setError("Email already in use. Try logging in instead."); } else if (err.code === "auth/invalid-email") { setError("Invalid email address"); } else if (err.code === "auth/weak-password") { setError("Password is too weak"); } else { setError("Failed to process plan selection: " + err.message); } } finally { setLoading(false); } } async function handleStep3() { setError(""); if (!data.walletAddress) { setError("Please enter a wallet address"); return; } if (!/^0x[0-9a-fA-F]{40}$/.test(data.walletAddress)) { setError("Invalid ETH address (must be 0x followed by 40 hex characters)"); return; } try { setLoading(true); const created = await createAddress({ address: data.walletAddress, label: data.walletLabel || undefined, }); set("createdAddressId", created.id); setStep(4); } catch (err) { setError(err.message); } finally { setLoading(false); } } async function handleStep4() { setError(""); const rules = []; if (data.alertIncomingTx) rules.push({ type: "incoming_tx" }); if (data.alertOutgoingTx) rules.push({ type: "outgoing_tx" }); if (data.alertLargeTransfer) { if (!data.largeTransferThreshold) { setError("Please enter a threshold for large transfers"); return; } rules.push({ type: "large_transfer", threshold: data.largeTransferThreshold }); } if (data.alertBalanceBelow) { if (!data.balanceBelowThreshold) { setError("Please enter a threshold for balance below"); return; } rules.push({ type: "balance_below", threshold: data.balanceBelowThreshold }); } if (rules.length === 0) { setStep(5); return; } try { setLoading(true); const created = []; for (const rule of rules) { const result = await createAlert(data.createdAddressId, rule); created.push(result); } set("alertsCreated", created); setStep(5); } catch (err) { setError(err.message); } finally { setLoading(false); } } async function handleStep5() { setError(""); const hasAny = data.discordWebhookUrl || data.slackWebhookUrl || data.notificationEmail; if (!hasAny) { setStep(6); return; } try { setLoading(true); await updateNotificationConfig({ notification_enabled: true, discord_webhook_url: data.discordWebhookUrl || undefined, slack_webhook_url: data.slackWebhookUrl || undefined, email: data.notificationEmail || undefined, }); set("notificationConfigured", true); setStep(6); } catch (err) { setError(err.message); } finally { setLoading(false); } } async function handleTestChannels() { setTestLoading(true); setTestResults(null); try { const results = await testNotificationChannels(); setTestResults(results); } catch (err) { setTestResults({ error: err.message }); } finally { setTestLoading(false); } } // ── Alert type limit helpers ────────────────────────────────────────────── function countSelectedAlerts() { let count = 0; if (data.alertIncomingTx) count++; if (data.alertOutgoingTx) count++; if (data.alertLargeTransfer) count++; if (data.alertBalanceBelow) count++; return count; } function canSelectMoreAlerts() { return tierLimits.maxAlertTypes > countSelectedAlerts(); } // ── Progress bar ────────────────────────────────────────────────────────── function ProgressBar() { return (
{STEPS.map((label, i) => { const stepNum = i + 1; const done = step > stepNum; const active = step === stepNum; const dotClass = done ? "progress-bar__dot--done" : active ? "progress-bar__dot--active" : "progress-bar__dot--pending"; return (
{i > 0 && (
)}
{done ? "\u2713" : stepNum}
{label}
); })}
); } // ── Step content ────────────────────────────────────────────────────────── function StepCreateAccount() { return ( <>

Create your account

set("email", v)} disabled={loading} placeholder="you@example.com" /> set("password", v)} disabled={loading} placeholder="At least 6 characters" /> set("confirmPassword", v)} disabled={loading} placeholder="Repeat your password" className="form-field--last" /> ); } function StepChoosePlan() { return ( <>

Choose your monitoring plan

Select the plan that works best for you. You can upgrade anytime.

set("selectedTier", tier)} selectedTier={data.selectedTier} /> ); } function StepAddWallet() { return ( <>

Add a wallet address

Enter the Ethereum address you want to monitor.

set("walletAddress", v)} disabled={loading} placeholder="0x..." /> set("walletLabel", v)} disabled={loading} placeholder="e.g. My main wallet" className="form-field--last" /> ); } function StepAlertRules() { const atLimit = !canSelectMoreAlerts(); const maxTypes = tierLimits.maxAlertTypes; return ( <>

Configure alert rules

Choose which events trigger notifications ({countSelectedAlerts()}/{maxTypes} selected). You can change these later.

set("alertIncomingTx", v)} label="Incoming transaction" disabled={!data.alertIncomingTx && atLimit} /> set("alertOutgoingTx", v)} label="Outgoing transaction" disabled={!data.alertOutgoingTx && atLimit} /> set("alertLargeTransfer", v)} label="Large transfer" disabled={!data.alertLargeTransfer && atLimit} > {data.alertLargeTransfer && (
set("largeTransferThreshold", v)} placeholder="Threshold (ETH)" min="0" step="0.01" />
)}
set("alertBalanceBelow", v)} label="Balance below" disabled={!data.alertBalanceBelow && atLimit} > {data.alertBalanceBelow && (
set("balanceBelowThreshold", v)} placeholder="Threshold (ETH)" min="0" step="0.01" />
)}
{atLimit && data.selectedTier !== "pro" && (

Your {data.selectedTier} plan allows {maxTypes} alert type{maxTypes !== 1 ? "s" : ""} per address. Upgrade for more.

)} ); } function StepNotifications() { const channels = tierLimits.channels; const canDiscord = channels.includes("discord"); const canSlack = channels.includes("slack"); return ( <>

Set up notifications

Add at least one channel so you receive alerts. All fields are optional.

set("notificationEmail", v)} disabled={loading} placeholder="you@example.com" className="form-field--last" />
set("discordWebhookUrl", v)} disabled={loading || !canDiscord} placeholder="https://discord.com/api/webhooks/..." /> {!canDiscord && (

Upgrade to Premium to enable Discord alerts

)}
set("slackWebhookUrl", v)} disabled={loading || !canSlack} placeholder="https://hooks.slack.com/services/..." /> {!canSlack && (

Upgrade to Pro to enable Slack alerts

)}
); } function StepDone() { const alertCount = data.alertsCreated.length; const hasNotif = data.notificationConfigured; return ( <>

You're all set!

Summary

  • Plan:{" "} {data.selectedTier === "pro" ? "Pro" : data.selectedTier === "premium" ? "Premium" : "Free Trial"}
  • Wallet address added:{" "} {data.walletAddress} {data.walletLabel && ` (${data.walletLabel})`}
  • Alert rules configured:{" "} {alertCount > 0 ? `${alertCount} rule${alertCount !== 1 ? "s" : ""}` : "None (skipped)"}
  • Notification channels:{" "} {hasNotif ? "Configured" : "Not set up (skipped)"}
{hasNotif && (
{testResults && (
{testResults.error ? (

{testResults.error}

) : (
    {Object.entries(testResults).map(([channel, result]) => (
  • {result.success ? "\u2713" : "\u2717"} {channel}:{" "} {result.message || (result.success ? "OK" : "Failed")}
  • ))}
)}
)}
)} ); } // ── Shared helpers ──────────────────────────────────────────────────────── function CheckboxRow({ checked, onChange, label, children, disabled }) { return (
{children}
); } // ── Footer navigation ───────────────────────────────────────────────────── function Footer() { if (step === 6) return null; const canSkip = step === 4 || step === 5; const canBack = step > 1 && step <= 5; async function handleNext() { setSkipWarning(""); if (step === 1) handleStep1(); else if (step === 2) await handleStep2(); else if (step === 3) await handleStep3(); else if (step === 4) await handleStep4(); else if (step === 5) await handleStep5(); } function handleSkip() { setError(""); setSkipWarning(""); setStep((s) => s + 1); } function handleBack() { setError(""); setSkipWarning(""); setStep((s) => s - 1); } const nextLabel = step === 1 ? "Create Account" : step === 2 ? !data.selectedTier ? "Continue" : data.selectedTier === "free" ? "Start Free Trial" : "Subscribe & Continue" : step === 5 ? "Finish" : "Next →"; return (
{canBack && ( )}
{canSkip && ( )}
); } // ── Render ──────────────────────────────────────────────────────────────── const stepContent = { 1: StepCreateAccount(), 2: StepChoosePlan(), 3: StepAddWallet(), 4: StepAlertRules(), 5: StepNotifications(), 6: StepDone(), }; return (

Koin Ping

{ProgressBar()} {error && (
{error}
)} {skipWarning && (
{skipWarning}
)}
{stepContent[step]} {Footer()}
{step === 1 && (

Already have an account?{" "} Log in here

)}
); }