diff --git a/frontend/src/components/TierPicker.jsx b/frontend/src/components/TierPicker.jsx index 790b784..db18200 100644 --- a/frontend/src/components/TierPicker.jsx +++ b/frontend/src/components/TierPicker.jsx @@ -11,11 +11,7 @@ const TIERS = [ "Configure alerts to fire on trigger events", "1 transaction alert type per trigger (email digest)", ], - disabledFeatures: [ - "Real-time Discord alerts", - "Real-time Telegram alerts", - "Real-time Slack alerts", - ], + disabledFeatures: [], }, { id: "premium", @@ -24,15 +20,12 @@ const TIERS = [ period: "/month", features: [ "Monitor 3 blockchain addresses", - "Configure alerts to fire on trigger events", + "Configure two types of rule-based alerts to fire on trigger events for each of the three addresses", "Daily email digest alert", "Real-time Discord alerts", "Real-time Telegram alerts", - "2 transaction alert types per monitored address", - ], - disabledFeatures: [ - "Real-time Slack alerts", ], + disabledFeatures: [], highlighted: true, }, { @@ -42,11 +35,11 @@ const TIERS = [ period: "/month", features: [ "Monitor unlimited blockchain addresses", - "Configure alerts to fire on trigger events", + "Configure unlimited alert rules to fire on unlimited events on any address", "Daily email digest alert", "Real-time Discord alerts", "Real-time Telegram alerts", - "Real-time Slack alerts", + "Real-time Slack alerts configurable for multiple Slack groups or channels", "Unlimited transaction alert types per monitored address", ], disabledFeatures: [], diff --git a/frontend/src/pages/subscribe/Subscribe.css b/frontend/src/pages/subscribe/Subscribe.css index 87fe097..b2b48f7 100644 --- a/frontend/src/pages/subscribe/Subscribe.css +++ b/frontend/src/pages/subscribe/Subscribe.css @@ -111,52 +111,6 @@ color: #888; } -/* Step 5 summary */ -.subscribe__summary { - background-color: #1e2e1e; - border: 1px solid #2d5a2d; - border-radius: var(--radius-lg); - padding: 1rem 1.25rem; - margin-bottom: 1.5rem; -} - -.subscribe__summary-title { - margin: 0 0 0.5rem; - color: #90ee90; - font-weight: bold; -} - -.subscribe__summary-list { - margin: 0; - padding-left: 1.25rem; - color: var(--color-text-label); - line-height: 1.8; -} - -/* Checkbox rows */ -.checkbox-row { - margin-bottom: 1rem; -} - -.checkbox-row__label { - display: flex; - align-items: center; - gap: 0.6rem; - cursor: pointer; - color: #ddd; -} - -.checkbox-row__input { - width: 16px; - height: 16px; - accent-color: var(--color-primary); -} - -.checkbox-row__nested { - margin-top: 0.5rem; - margin-left: 1.75rem; -} - /* Footer navigation */ .subscribe__footer { display: flex; @@ -167,20 +121,6 @@ border-top: 1px solid var(--color-border-light); } -/* Test results */ -.test-result { - font-size: 0.9rem; - margin-bottom: 0.25rem; -} - -.test-result--success { - color: #90ee90; -} - -.test-result--failure { - color: var(--color-error); -} - /* Step subtitle */ .subscribe__subtitle { color: #aaa; @@ -188,7 +128,7 @@ font-size: 0.9rem; } -/* Subscribe card (Step 2) */ +/* Subscribe card (Step 2 tier cards) */ .subscribe-card { background-color: var(--color-bg-card, #1a1a2e); border: 1px solid var(--color-border-light); @@ -241,29 +181,6 @@ font-style: italic; } -/* Tier limit hints */ -.subscribe__tier-hint { - font-size: 0.82rem; - color: #ffcc44; - margin-top: 0.5rem; -} - -/* Disabled channel group */ -.subscribe__channel-disabled { - opacity: 0.5; - pointer-events: none; -} - -.subscribe__channel-disabled .subscribe__tier-hint { - pointer-events: auto; - opacity: 1; -} - -/* Disabled checkbox row */ -.checkbox-row--disabled { - opacity: 0.5; -} - /* ── Subscribe Responsive ────────────────────────────────── */ @media (max-width: 480px) { @@ -284,4 +201,4 @@ height: 26px; font-size: 0.75rem; } -} \ No newline at end of file +} diff --git a/frontend/src/pages/subscribe/Subscribe.jsx b/frontend/src/pages/subscribe/Subscribe.jsx index 3a04cf7..4031120 100644 --- a/frontend/src/pages/subscribe/Subscribe.jsx +++ b/frontend/src/pages/subscribe/Subscribe.jsx @@ -1,32 +1,17 @@ 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"; + 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", -]; +const STEPS = ["Create Account", "Choose Plan"]; export default function Subscribe() { const { currentUser, signup } = useAuth(); @@ -34,52 +19,22 @@ export default function Subscribe() { 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]); - + // Handle Stripe payment returns useEffect(() => { const payment = searchParams.get("payment"); const sessionId = searchParams.get("session_id"); @@ -93,9 +48,8 @@ export default function Subscribe() { 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. + // Phase 1: no account yet — create it. Auth state change remounts the + // component (App.jsx swaps route trees) and Phase 2 runs on the next mount. if (!currentUser) { const savedEmail = sessionStorage.getItem("kp_onboard_email"); const savedPw = sessionStorage.getItem("kp_onboard_pw"); @@ -116,7 +70,7 @@ export default function Subscribe() { return; } - // Phase 2: authenticated — verify the checkout and activate subscription. + // Phase 2: authenticated — verify the checkout and go to the dashboard. setSearchParams({}, { replace: true }); setLoading(true); sessionStorage.removeItem("kp_onboard_email"); @@ -124,14 +78,14 @@ export default function Subscribe() { verifyCheckoutSession(sessionId) .then(() => { - setStep(3); + navigate("/addresses", { replace: true }); }) .catch((err) => { setError("Payment verification failed: " + err.message); setStep(2); }) .finally(() => setLoading(false)); - }, [currentUser, searchParams, setSearchParams, signup]); + }, [currentUser, searchParams, setSearchParams, signup, navigate]); // ── Step handlers ───────────────────────────────────────────────────────── @@ -166,13 +120,16 @@ export default function Subscribe() { await signup(data.email, data.password); } await activateFreeTier(); - setStep(3); + navigate("/addresses", { replace: true }); return; } sessionStorage.setItem("kp_onboard_email", data.email); sessionStorage.setItem("kp_onboard_pw", data.password); - const { url } = await createOnboardingCheckout(data.email, data.selectedTier); + const { url } = await createOnboardingCheckout( + data.email, + data.selectedTier, + ); window.location.href = url; } catch (err) { if (err.code === "auth/email-already-in-use") { @@ -189,125 +146,6 @@ export default function Subscribe() { } } - 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() { @@ -327,10 +165,11 @@ export default function Subscribe() {
{i > 0 && (
)}
@@ -338,10 +177,11 @@ export default function Subscribe() { {done ? "\u2713" : stepNum}
{label}
@@ -403,352 +243,45 @@ export default function Subscribe() { ); } - 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 + const nextLabel = + step === 1 + ? "Create Account" + : !data.selectedTier ? "Continue" : data.selectedTier === "free" ? "Start Free Trial" - : "Subscribe & Continue" - : step === 5 - ? "Finish" - : "Next →"; + : "Subscribe & Continue"; return (
- {canBack && ( - )}
-
- {canSkip && ( - - )} - -
+
); } @@ -758,26 +291,18 @@ export default function Subscribe() { const stepContent = { 1: StepCreateAccount(), 2: StepChoosePlan(), - 3: StepAddWallet(), - 4: StepAlertRules(), - 5: StepNotifications(), - 6: StepDone(), }; return (
-
+

Koin Ping

{ProgressBar()} - {error && ( -
{error}
- )} - - {skipWarning && ( -
{skipWarning}
- )} + {error &&
{error}
}
{stepContent[step]} @@ -786,8 +311,7 @@ export default function Subscribe() { {step === 1 && (

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

)}