Compare commits

..

3 Commits

Author SHA1 Message Date
KS Jannette
0d7bc65995 adjust flow 2026-03-28 23:09:03 -04:00
KS Jannette
05453895b9 m 2026-03-28 22:54:52 -04:00
S Jannette
14ed0a23a6 Merge pull request #28 from kjannette/mega-blast
Mega blast
2026-03-28 22:34:56 -04:00
3 changed files with 52 additions and 618 deletions

View File

@@ -11,11 +11,7 @@ const TIERS = [
"Configure alerts to fire on trigger events", "Configure alerts to fire on trigger events",
"1 transaction alert type per trigger (email digest)", "1 transaction alert type per trigger (email digest)",
], ],
disabledFeatures: [ disabledFeatures: [],
"Real-time Discord alerts",
"Real-time Telegram alerts",
"Real-time Slack alerts",
],
}, },
{ {
id: "premium", id: "premium",
@@ -24,15 +20,12 @@ const TIERS = [
period: "/month", period: "/month",
features: [ features: [
"Monitor 3 blockchain addresses", "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", "Daily email digest alert",
"Real-time Discord alerts", "Real-time Discord alerts",
"Real-time Telegram alerts", "Real-time Telegram alerts",
"2 transaction alert types per monitored address",
],
disabledFeatures: [
"Real-time Slack alerts",
], ],
disabledFeatures: [],
highlighted: true, highlighted: true,
}, },
{ {
@@ -42,11 +35,11 @@ const TIERS = [
period: "/month", period: "/month",
features: [ features: [
"Monitor unlimited blockchain addresses", "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", "Daily email digest alert",
"Real-time Discord alerts", "Real-time Discord alerts",
"Real-time Telegram 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", "Unlimited transaction alert types per monitored address",
], ],
disabledFeatures: [], disabledFeatures: [],

View File

@@ -111,52 +111,6 @@
color: #888; 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 */ /* Footer navigation */
.subscribe__footer { .subscribe__footer {
display: flex; display: flex;
@@ -167,20 +121,6 @@
border-top: 1px solid var(--color-border-light); 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 */ /* Step subtitle */
.subscribe__subtitle { .subscribe__subtitle {
color: #aaa; color: #aaa;
@@ -188,7 +128,7 @@
font-size: 0.9rem; font-size: 0.9rem;
} }
/* Subscribe card (Step 2) */ /* Subscribe card (Step 2 tier cards) */
.subscribe-card { .subscribe-card {
background-color: var(--color-bg-card, #1a1a2e); background-color: var(--color-bg-card, #1a1a2e);
border: 1px solid var(--color-border-light); border: 1px solid var(--color-border-light);
@@ -241,29 +181,6 @@
font-style: italic; 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 ────────────────────────────────── */ /* ── Subscribe Responsive ────────────────────────────────── */
@media (max-width: 480px) { @media (max-width: 480px) {

View File

@@ -1,32 +1,17 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useNavigate, useSearchParams } from "react-router-dom"; import { useNavigate, useSearchParams } from "react-router-dom";
import { useAuth } from "../../contexts/AuthContext"; import { useAuth } from "../../contexts/AuthContext";
import { createAddress, getAddresses } from "../../api/addresses";
import { createAlert } from "../../api/alerts";
import { import {
updateNotificationConfig, createOnboardingCheckout,
testNotificationChannels, verifyCheckoutSession,
} from "../../api/notificationConfig"; activateFreeTier,
import { createOnboardingCheckout, verifyCheckoutSession, activateFreeTier } from "../../api/stripe"; } from "../../api/stripe";
import Input from "../../components/Input"; import Input from "../../components/Input";
import Button from "../../components/Button"; import Button from "../../components/Button";
import TierPicker from "../../components/TierPicker"; import TierPicker from "../../components/TierPicker";
import "./Subscribe.css"; import "./Subscribe.css";
const TIER_LIMITS = { const STEPS = ["Create Account", "Choose Plan"];
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() { export default function Subscribe() {
const { currentUser, signup } = useAuth(); const { currentUser, signup } = useAuth();
@@ -34,52 +19,22 @@ export default function Subscribe() {
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const hasPaymentReturn = searchParams.get("payment") === "success"; const hasPaymentReturn = searchParams.get("payment") === "success";
const isUpgrade = searchParams.get("upgrade") === "true";
const [step, setStep] = useState(hasPaymentReturn || currentUser ? 2 : 1); const [step, setStep] = useState(hasPaymentReturn || currentUser ? 2 : 1);
const [loading, setLoading] = useState(hasPaymentReturn); const [loading, setLoading] = useState(hasPaymentReturn);
const [error, setError] = useState(""); const [error, setError] = useState("");
const [skipWarning, setSkipWarning] = useState("");
const [testResults, setTestResults] = useState(null);
const [testLoading, setTestLoading] = useState(false);
const [data, setData] = useState({ const [data, setData] = useState({
selectedTier: "", selectedTier: "",
email: "", email: "",
password: "", password: "",
confirmPassword: "", 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) { function set(field, value) {
setData((prev) => ({ ...prev, [field]: value })); setData((prev) => ({ ...prev, [field]: value }));
} }
const tierLimits = TIER_LIMITS[data.selectedTier] || TIER_LIMITS.free; // Handle Stripe payment returns
useEffect(() => {
if (!currentUser || isUpgrade) return;
getAddresses()
.then((addresses) => {
if (addresses.length > 0) {
navigate("/addresses", { replace: true });
}
})
.catch(() => { });
}, [currentUser, navigate, isUpgrade]);
useEffect(() => { useEffect(() => {
const payment = searchParams.get("payment"); const payment = searchParams.get("payment");
const sessionId = searchParams.get("session_id"); const sessionId = searchParams.get("session_id");
@@ -93,9 +48,8 @@ export default function Subscribe() {
if (payment !== "success" || !sessionId) return; if (payment !== "success" || !sessionId) return;
// Phase 1: no account yet — create it. This triggers an auth state change // Phase 1: no account yet — create it. Auth state change remounts the
// which remounts the component (App.jsx swaps route trees). The URL params // component (App.jsx swaps route trees) and Phase 2 runs on the next mount.
// are preserved so Phase 2 runs on the next mount.
if (!currentUser) { if (!currentUser) {
const savedEmail = sessionStorage.getItem("kp_onboard_email"); const savedEmail = sessionStorage.getItem("kp_onboard_email");
const savedPw = sessionStorage.getItem("kp_onboard_pw"); const savedPw = sessionStorage.getItem("kp_onboard_pw");
@@ -116,7 +70,7 @@ export default function Subscribe() {
return; return;
} }
// Phase 2: authenticated — verify the checkout and activate subscription. // Phase 2: authenticated — verify the checkout and go to the dashboard.
setSearchParams({}, { replace: true }); setSearchParams({}, { replace: true });
setLoading(true); setLoading(true);
sessionStorage.removeItem("kp_onboard_email"); sessionStorage.removeItem("kp_onboard_email");
@@ -124,14 +78,14 @@ export default function Subscribe() {
verifyCheckoutSession(sessionId) verifyCheckoutSession(sessionId)
.then(() => { .then(() => {
setStep(3); navigate("/addresses", { replace: true });
}) })
.catch((err) => { .catch((err) => {
setError("Payment verification failed: " + err.message); setError("Payment verification failed: " + err.message);
setStep(2); setStep(2);
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}, [currentUser, searchParams, setSearchParams, signup]); }, [currentUser, searchParams, setSearchParams, signup, navigate]);
// ── Step handlers ───────────────────────────────────────────────────────── // ── Step handlers ─────────────────────────────────────────────────────────
@@ -166,13 +120,16 @@ export default function Subscribe() {
await signup(data.email, data.password); await signup(data.email, data.password);
} }
await activateFreeTier(); await activateFreeTier();
setStep(3); navigate("/addresses", { replace: true });
return; return;
} }
sessionStorage.setItem("kp_onboard_email", data.email); sessionStorage.setItem("kp_onboard_email", data.email);
sessionStorage.setItem("kp_onboard_pw", data.password); 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; window.location.href = url;
} catch (err) { } catch (err) {
if (err.code === "auth/email-already-in-use") { 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 ────────────────────────────────────────────────────────── // ── Progress bar ──────────────────────────────────────────────────────────
function ProgressBar() { function ProgressBar() {
@@ -327,10 +165,11 @@ export default function Subscribe() {
<div key={label} className="progress-bar__step"> <div key={label} className="progress-bar__step">
{i > 0 && ( {i > 0 && (
<div <div
className={`progress-bar__connector ${done || active className={`progress-bar__connector ${
? "progress-bar__connector--active" done || active
: "progress-bar__connector--inactive" ? "progress-bar__connector--active"
}`} : "progress-bar__connector--inactive"
}`}
/> />
)} )}
<div> <div>
@@ -338,10 +177,11 @@ export default function Subscribe() {
{done ? "\u2713" : stepNum} {done ? "\u2713" : stepNum}
</div> </div>
<div <div
className={`progress-bar__label ${active className={`progress-bar__label ${
? "progress-bar__label--active" active
: "progress-bar__label--inactive" ? "progress-bar__label--active"
}`} : "progress-bar__label--inactive"
}`}
> >
{label} {label}
</div> </div>
@@ -403,352 +243,45 @@ export default function Subscribe() {
); );
} }
function StepAddWallet() {
return (
<>
<h2 className="mb-sm">Add a wallet address</h2>
<p className="subscribe__subtitle">
Enter the Ethereum address you want to monitor.
</p>
<Input
label="ETH Address"
value={data.walletAddress}
onChange={(v) => set("walletAddress", v)}
disabled={loading}
placeholder="0x..."
/>
<Input
label="Label (optional)"
value={data.walletLabel}
onChange={(v) => 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 (
<>
<h2 className="mb-sm">Configure alert rules</h2>
<p className="subscribe__subtitle">
Choose which events trigger notifications ({countSelectedAlerts()}/{maxTypes} selected).
You can change these later.
</p>
<CheckboxRow
checked={data.alertIncomingTx}
onChange={(v) => set("alertIncomingTx", v)}
label="Incoming transaction"
disabled={!data.alertIncomingTx && atLimit}
/>
<CheckboxRow
checked={data.alertOutgoingTx}
onChange={(v) => set("alertOutgoingTx", v)}
label="Outgoing transaction"
disabled={!data.alertOutgoingTx && atLimit}
/>
<CheckboxRow
checked={data.alertLargeTransfer}
onChange={(v) => set("alertLargeTransfer", v)}
label="Large transfer"
disabled={!data.alertLargeTransfer && atLimit}
>
{data.alertLargeTransfer && (
<div className="checkbox-row__nested">
<Input
type="number"
label=""
value={data.largeTransferThreshold}
onChange={(v) => set("largeTransferThreshold", v)}
placeholder="Threshold (ETH)"
min="0"
step="0.01"
/>
</div>
)}
</CheckboxRow>
<CheckboxRow
checked={data.alertBalanceBelow}
onChange={(v) => set("alertBalanceBelow", v)}
label="Balance below"
disabled={!data.alertBalanceBelow && atLimit}
>
{data.alertBalanceBelow && (
<div className="checkbox-row__nested">
<Input
type="number"
label=""
value={data.balanceBelowThreshold}
onChange={(v) => set("balanceBelowThreshold", v)}
placeholder="Threshold (ETH)"
min="0"
step="0.01"
/>
</div>
)}
</CheckboxRow>
{atLimit && data.selectedTier !== "pro" && (
<p className="subscribe__tier-hint">
Your {data.selectedTier} plan allows {maxTypes} alert type{maxTypes !== 1 ? "s" : ""} per address. Upgrade for more.
</p>
)}
</>
);
}
function StepNotifications() {
const channels = tierLimits.channels;
const canDiscord = channels.includes("discord");
const canSlack = channels.includes("slack");
return (
<>
<h2 className="mb-sm">Set up notifications</h2>
<p className="subscribe__subtitle">
Add at least one channel so you receive alerts. All fields are optional.
</p>
<Input
label="Email address for alerts"
type="email"
value={data.notificationEmail}
onChange={(v) => set("notificationEmail", v)}
disabled={loading}
placeholder="you@example.com"
className="form-field--last"
/>
<div className={`mb-md${!canDiscord ? " subscribe__channel-disabled" : ""}`}>
<label className="form-label">
Discord Webhook URL{" "}
<a
href="https://support.discord.com/hc/en-us/articles/228383668"
target="_blank"
rel="noreferrer"
className="help-link"
>
(how to get one)
</a>
</label>
<Input
label=""
type="url"
value={data.discordWebhookUrl}
onChange={(v) => set("discordWebhookUrl", v)}
disabled={loading || !canDiscord}
placeholder="https://discord.com/api/webhooks/..."
/>
{!canDiscord && (
<p className="subscribe__tier-hint">Upgrade to Premium to enable Discord alerts</p>
)}
</div>
<div className={`mb-md${!canSlack ? " subscribe__channel-disabled" : ""}`}>
<label className="form-label">
Slack Webhook URL{" "}
<a
href="https://api.slack.com/messaging/webhooks"
target="_blank"
rel="noreferrer"
className="help-link"
>
(how to get one)
</a>
</label>
<Input
label=""
type="url"
value={data.slackWebhookUrl}
onChange={(v) => set("slackWebhookUrl", v)}
disabled={loading || !canSlack}
placeholder="https://hooks.slack.com/services/..."
/>
{!canSlack && (
<p className="subscribe__tier-hint">Upgrade to Pro to enable Slack alerts</p>
)}
</div>
</>
);
}
function StepDone() {
const alertCount = data.alertsCreated.length;
const hasNotif = data.notificationConfigured;
return (
<>
<h2 className="mb-md">You're all set!</h2>
<div className="subscribe__summary">
<p className="subscribe__summary-title">Summary</p>
<ul className="subscribe__summary-list">
<li>
Plan:{" "}
<span className="text-white">
{data.selectedTier === "pro" ? "Pro" : data.selectedTier === "premium" ? "Premium" : "Free Trial"}
</span>
</li>
<li>
Wallet address added:{" "}
<span className="text-mono text-white-sm">
{data.walletAddress}
</span>
{data.walletLabel && ` (${data.walletLabel})`}
</li>
<li>
Alert rules configured:{" "}
<span className="text-white">
{alertCount > 0 ? `${alertCount} rule${alertCount !== 1 ? "s" : ""}` : "None (skipped)"}
</span>
</li>
<li>
Notification channels:{" "}
<span className="text-white">
{hasNotif ? "Configured" : "Not set up (skipped)"}
</span>
</li>
</ul>
</div>
{hasNotif && (
<div className="mb-lg">
<Button
onClick={handleTestChannels}
disabled={testLoading}
variant="ghost"
>
{testLoading ? "Testing..." : "Test All Channels"}
</Button>
{testResults && (
<div className="mt-md">
{testResults.error ? (
<p className="text-error">{testResults.error}</p>
) : (
<ul className="list-unstyled">
{Object.entries(testResults).map(([channel, result]) => (
<li
key={channel}
className={`test-result ${result.success ? "test-result--success" : "test-result--failure"}`}
>
{result.success ? "\u2713" : "\u2717"} {channel}:{" "}
{result.message || (result.success ? "OK" : "Failed")}
</li>
))}
</ul>
)}
</div>
)}
</div>
)}
<Button onClick={() => navigate("/addresses")} className="btn--lg text-bold">
Go to Dashboard
</Button>
</>
);
}
// ── Shared helpers ────────────────────────────────────────────────────────
function CheckboxRow({ checked, onChange, label, children, disabled }) {
return (
<div className={`checkbox-row${disabled ? " checkbox-row--disabled" : ""}`}>
<label className="checkbox-row__label">
<input
type="checkbox"
checked={checked}
onChange={(e) => onChange(e.target.checked)}
className="checkbox-row__input"
disabled={disabled}
/>
{label}
</label>
{children}
</div>
);
}
// ── Footer navigation ───────────────────────────────────────────────────── // ── Footer navigation ─────────────────────────────────────────────────────
function Footer() { function Footer() {
if (step === 6) return null;
const canSkip = step === 4 || step === 5;
const canBack = step > 1 && step <= 5;
async function handleNext() { async function handleNext() {
setSkipWarning("");
if (step === 1) handleStep1(); if (step === 1) handleStep1();
else if (step === 2) await handleStep2(); 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() { function handleBack() {
setError(""); setError("");
setSkipWarning("");
setStep((s) => s - 1); setStep((s) => s - 1);
} }
const nextLabel = step === 1 const nextLabel =
? "Create Account" step === 1
: step === 2 ? "Create Account"
? !data.selectedTier : !data.selectedTier
? "Continue" ? "Continue"
: data.selectedTier === "free" : data.selectedTier === "free"
? "Start Free Trial" ? "Start Free Trial"
: "Subscribe & Continue" : "Subscribe & Continue";
: step === 5
? "Finish"
: "Next →";
return ( return (
<div className="subscribe__footer"> <div className="subscribe__footer">
<div> <div>
{canBack && ( {step === 2 && (
<Button <Button onClick={handleBack} disabled={loading} variant="ghost">
onClick={handleBack}
disabled={loading}
variant="ghost"
>
Back Back
</Button> </Button>
)} )}
</div> </div>
<div className="flex gap-md"> <Button
{canSkip && ( onClick={handleNext}
<Button disabled={loading || (step === 2 && !data.selectedTier)}
onClick={handleSkip} className="text-bold"
disabled={loading} >
variant="ghost" {loading ? "Please wait..." : nextLabel}
> </Button>
Skip for now
</Button>
)}
<Button
onClick={handleNext}
disabled={loading || (step === 2 && !data.selectedTier)}
className="text-bold"
>
{loading ? "Please wait..." : nextLabel}
</Button>
</div>
</div> </div>
); );
} }
@@ -758,26 +291,18 @@ export default function Subscribe() {
const stepContent = { const stepContent = {
1: StepCreateAccount(), 1: StepCreateAccount(),
2: StepChoosePlan(), 2: StepChoosePlan(),
3: StepAddWallet(),
4: StepAlertRules(),
5: StepNotifications(),
6: StepDone(),
}; };
return ( return (
<div className="subscribe"> <div className="subscribe">
<div className={`subscribe__container${step === 2 ? " subscribe__container--wide" : ""}`}> <div
className={`subscribe__container${step === 2 ? " subscribe__container--wide" : ""}`}
>
<h1 className="subscribe__title">Koin Ping</h1> <h1 className="subscribe__title">Koin Ping</h1>
{ProgressBar()} {ProgressBar()}
{error && ( {error && <div className="alert alert--error">{error}</div>}
<div className="alert alert--error">{error}</div>
)}
{skipWarning && (
<div className="alert alert--warning">{skipWarning}</div>
)}
<div className="subscribe__card"> <div className="subscribe__card">
{stepContent[step]} {stepContent[step]}
@@ -786,8 +311,7 @@ export default function Subscribe() {
{step === 1 && ( {step === 1 && (
<p className="subscribe__login-link"> <p className="subscribe__login-link">
Already have an account?{" "} Already have an account? <a href="/login">Log in here</a>
<a href="/login">Log in here</a>
</p> </p>
)} )}
</div> </div>