/** * Subscribe / Subscribe Wizard * * 5-step guided flow: Create Account -> Add Wallet -> Alert Rules -> Notifications -> Done * After account creation, user is redirected to Stripe Checkout for payment. * On successful payment, they return here at step 2 (Add Wallet). */ 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 { createCheckoutSession, getSubscriptionStatus, verifyCheckoutSession } from "../../api/stripe"; import Input from "../../components/Input"; import Button from "../../components/Button"; import "./Subscribe.css"; const STEPS = [ "Create Account", "Add Wallet", "Alert Rules", "Notifications", "Done", ]; export default function Subscribe() { const { currentUser, signup } = useAuth(); const navigate = useNavigate(); const [searchParams, setSearchParams] = useSearchParams(); const [step, setStep] = useState(1); const [loading, setLoading] = useState(false); const [error, setError] = useState(""); const [skipWarning, setSkipWarning] = useState(""); const [testResults, setTestResults] = useState(null); const [testLoading, setTestLoading] = useState(false); const [data, setData] = useState({ 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 })); } useEffect(() => { if (!currentUser) return; getAddresses() .then((addresses) => { if (addresses.length > 0) { navigate("/addresses", { replace: true }); } }) .catch(() => { }); }, [currentUser, navigate]); // Handle Stripe redirect back from checkout useEffect(() => { if (!currentUser) return; const payment = searchParams.get("payment"); const sessionId = searchParams.get("session_id"); if (payment === "success" && sessionId) { setSearchParams({}, { replace: true }); setLoading(true); verifyCheckoutSession(sessionId) .then(() => { setStep(2); }) .catch((err) => { setError("Payment verification failed: " + err.message); setStep(1); }) .finally(() => setLoading(false)); } else if (payment === "cancelled") { setSearchParams({}, { replace: true }); setStep(1); setError("Payment was cancelled. Please try again."); } }, [currentUser, searchParams, setSearchParams]); // ── Step handlers ───────────────────────────────────────────────────────── async 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; } try { setLoading(true); if (!currentUser) { await signup(data.email, data.password); } const status = await getSubscriptionStatus(); if (status.subscription_status === "active" || status.subscription_status === "trialing") { setStep(2); return; } const { url } = await createCheckoutSession(); 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 create account: " + err.message); } setLoading(false); } } async function handleStep2() { 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(3); } catch (err) { setError(err.message); } finally { setLoading(false); } } async function handleStep3() { 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(4); 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(4); } catch (err) { setError(err.message); } finally { setLoading(false); } } async function handleStep4() { setError(""); const hasAny = data.discordWebhookUrl || data.slackWebhookUrl || data.notificationEmail; if (!hasAny) { setStep(5); 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(5); } 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); } } // ── Progress bar ────────────────────────────────────────────────────────── function ProgressBar() { return (
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 Step3() { return ( <>Choose which events trigger notifications. You can change these later.
Add at least one channel so you receive alerts. All fields are optional.
Summary
{testResults.error}
) : (Already have an account?{" "} Log in here
)}