addition Stripe payment integration work
Some checks are pending
check / check (push) Waiting to run

This commit is contained in:
KS Jannette
2026-03-04 21:38:36 -05:00
parent 9ad414313b
commit 239ea7db85
6 changed files with 54 additions and 97 deletions

View File

@@ -2,7 +2,6 @@ Start DB:
brew services start postgresql@15 brew services start postgresql@15
From the backend-go directory, you have a few options: From the backend-go directory, you have a few options:
Option 1: Single command (both API + poller) Option 1: Single command (both API + poller)
@@ -10,9 +9,9 @@ cd /Users/kjannette/workspace/koin_ping_0.2.0/backend-gomake dev-all
Option 2: Two separate terminals Option 2: Two separate terminals
Terminal 1 (API server): Terminal 1 (API server):
cd /Users/kjannette/workspace/koin_ping_0.2.0/backend-gogo run ./cmd/api cd /Users/kjannette/workspace/koin_ping_0.2.0/backend-go go run ./cmd/api
Terminal 2 (Poller): Terminal 2 (Poller):
cd /Users/kjannette/workspace/koin_ping_0.2.0/backend-gogo run ./cmd/poller cd /Users/kjannette/workspace/koin_ping_0.2.0/backend-go go run ./cmd/poller
make run — Builds and runs the API server. make run — Builds and runs the API server.
make dev — Runs the API server with auto-reload via air (falls back to go run if air isn't installed). make dev — Runs the API server with auto-reload via air (falls back to go run if air isn't installed).

View File

@@ -46,8 +46,8 @@ func (h *StripeHandler) CreateCheckoutSession(w http.ResponseWriter, r *http.Req
Quantity: stripe.Int64(1), Quantity: stripe.Int64(1),
}, },
}, },
SuccessURL: stripe.String(h.cfg.FrontendURL + "/onboarding?payment=success&session_id={CHECKOUT_SESSION_ID}"), SuccessURL: stripe.String(h.cfg.FrontendURL + "/subscribe?payment=success&session_id={CHECKOUT_SESSION_ID}"),
CancelURL: stripe.String(h.cfg.FrontendURL + "/onboarding?payment=cancelled"), CancelURL: stripe.String(h.cfg.FrontendURL + "/subscribe?payment=cancelled"),
ClientReferenceID: stripe.String(userID), ClientReferenceID: stripe.String(userID),
CustomerEmail: stripe.String(user.Email), CustomerEmail: stripe.String(user.Email),
} }

View File

@@ -16,7 +16,7 @@ export default function App() {
<Routes> <Routes>
<Route path="/login" element={<Login />} /> <Route path="/login" element={<Login />} />
<Route path="/signup" element={<Signup />} /> <Route path="/signup" element={<Signup />} />
<Route path="/onboarding" element={<Onboarding />} /> <Route path="/subscribe" element={<Onboarding />} />
<Route path="*" element={<Navigate to="/login" />} /> <Route path="*" element={<Navigate to="/login" />} />
</Routes> </Routes>
); );
@@ -30,7 +30,7 @@ export default function App() {
<Route path="/addresses" element={<Addresses />} /> <Route path="/addresses" element={<Addresses />} />
<Route path="/alerts" element={<Alerts />} /> <Route path="/alerts" element={<Alerts />} />
<Route path="/alertevents" element={<AlertHistory />} /> <Route path="/alertevents" element={<AlertHistory />} />
<Route path="/onboarding" element={<Onboarding />} /> <Route path="/subscribe" element={<Onboarding />} />
<Route path="*" element={<Navigate to="/addresses" />} /> <Route path="*" element={<Navigate to="/addresses" />} />
</Routes> </Routes>
</div> </div>

View File

@@ -1,6 +1,6 @@
.input__label { .input__label {
display: block; display: block;
margin-bottom: 0.4rem; margin: 0.8rem 0rem 0.4rem 0rem;
font-size: 0.9rem; font-size: 0.9rem;
} }
@@ -18,4 +18,4 @@
outline: none; outline: none;
border-color: var(--color-primary); border-color: var(--color-primary);
box-shadow: 0 0 0 2px rgba(0, 102, 204, 0.25); box-shadow: 0 0 0 2px rgba(0, 102, 204, 0.25);
} }

View File

@@ -1,7 +1,9 @@
/** /**
* Onboarding Wizard * Subscribe / Onboarding Wizard
* *
* 6-step guided flow: Create Account -> Subscribe -> Add Wallet -> Alert Rules -> Notifications -> Done * 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 { useState, useEffect } from "react";
@@ -20,7 +22,6 @@ import "./Onboarding.css";
const STEPS = [ const STEPS = [
"Create Account", "Create Account",
"Subscribe",
"Add Wallet", "Add Wallet",
"Alert Rules", "Alert Rules",
"Notifications", "Notifications",
@@ -80,11 +81,11 @@ export default function Onboarding() {
const payment = searchParams.get("payment"); const payment = searchParams.get("payment");
if (payment === "success") { if (payment === "success") {
setSearchParams({}, { replace: true }); setSearchParams({}, { replace: true });
setStep(3); setStep(2);
} else if (payment === "cancelled") { } else if (payment === "cancelled") {
setSearchParams({}, { replace: true }); setSearchParams({}, { replace: true });
setStep(2); setStep(1);
setError("Payment was cancelled. Please subscribe to continue."); setError("Payment was cancelled. Please try again.");
} }
}, [currentUser, searchParams, setSearchParams]); }, [currentUser, searchParams, setSearchParams]);
@@ -104,47 +105,33 @@ export default function Onboarding() {
setError("Password must be at least 6 characters"); setError("Password must be at least 6 characters");
return; return;
} }
if (!currentUser) {
try {
setLoading(true);
await signup(data.email, data.password);
} 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);
return;
} finally {
setLoading(false);
}
}
setStep(2);
}
async function handleStep2Subscribe() {
setError("");
try { try {
setLoading(true); setLoading(true);
if (!currentUser) {
await signup(data.email, data.password);
}
const status = await getSubscriptionStatus(); const status = await getSubscriptionStatus();
if (status.subscription_status === "active" || status.subscription_status === "trialing") { if (status.subscription_status === "active" || status.subscription_status === "trialing") {
setStep(3); setStep(2);
return; return;
} }
const { url } = await createCheckoutSession(); const { url } = await createCheckoutSession();
window.location.href = url; window.location.href = url;
} catch (err) { } catch (err) {
setError(err.message); 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); setLoading(false);
} }
} }
async function handleStep3() { async function handleStep2() {
setError(""); setError("");
if (!data.walletAddress) { if (!data.walletAddress) {
setError("Please enter a wallet address"); setError("Please enter a wallet address");
@@ -161,7 +148,7 @@ export default function Onboarding() {
label: data.walletLabel || undefined, label: data.walletLabel || undefined,
}); });
set("createdAddressId", created.id); set("createdAddressId", created.id);
setStep(4); setStep(3);
} catch (err) { } catch (err) {
setError(err.message); setError(err.message);
} finally { } finally {
@@ -169,7 +156,7 @@ export default function Onboarding() {
} }
} }
async function handleStep4() { async function handleStep3() {
setError(""); setError("");
const rules = []; const rules = [];
if (data.alertIncomingTx) rules.push({ type: "incoming_tx" }); if (data.alertIncomingTx) rules.push({ type: "incoming_tx" });
@@ -190,7 +177,7 @@ export default function Onboarding() {
} }
if (rules.length === 0) { if (rules.length === 0) {
setStep(5); setStep(4);
return; return;
} }
@@ -202,7 +189,7 @@ export default function Onboarding() {
created.push(result); created.push(result);
} }
set("alertsCreated", created); set("alertsCreated", created);
setStep(5); setStep(4);
} catch (err) { } catch (err) {
setError(err.message); setError(err.message);
} finally { } finally {
@@ -210,12 +197,12 @@ export default function Onboarding() {
} }
} }
async function handleStep5() { async function handleStep4() {
setError(""); setError("");
const hasAny = const hasAny =
data.discordWebhookUrl || data.slackWebhookUrl || data.notificationEmail; data.discordWebhookUrl || data.slackWebhookUrl || data.notificationEmail;
if (!hasAny) { if (!hasAny) {
setStep(6); setStep(5);
return; return;
} }
try { try {
@@ -227,7 +214,7 @@ export default function Onboarding() {
email: data.notificationEmail || undefined, email: data.notificationEmail || undefined,
}); });
set("notificationConfigured", true); set("notificationConfigured", true);
setStep(6); setStep(5);
} catch (err) { } catch (err) {
setError(err.message); setError(err.message);
} finally { } finally {
@@ -331,33 +318,6 @@ export default function Onboarding() {
} }
function Step2() { function Step2() {
return (
<>
<h2 className="mb-sm">Subscribe to Koin Ping</h2>
<p className="onboarding__subtitle">
A subscription is required to use Koin Ping.
</p>
<div className="subscribe-card">
<div className="subscribe-card__price">
<span className="subscribe-card__amount">$1.99</span>
<span className="subscribe-card__period">/month</span>
</div>
<ul className="subscribe-card__features">
<li>Unlimited wallet monitoring</li>
<li>Real-time alert notifications</li>
<li>Discord, Slack, Telegram & email alerts</li>
<li>Email digest reports</li>
</ul>
<p className="subscribe-card__commitment">
6-month minimum commitment
</p>
</div>
</>
);
}
function Step3() {
return ( return (
<> <>
<h2 className="mb-sm">Add a wallet address</h2> <h2 className="mb-sm">Add a wallet address</h2>
@@ -383,7 +343,7 @@ export default function Onboarding() {
); );
} }
function Step4() { function Step3() {
return ( return (
<> <>
<h2 className="mb-sm">Configure alert rules</h2> <h2 className="mb-sm">Configure alert rules</h2>
@@ -443,7 +403,7 @@ export default function Onboarding() {
); );
} }
function Step5() { function Step4() {
return ( return (
<> <>
<h2 className="mb-sm">Set up notifications</h2> <h2 className="mb-sm">Set up notifications</h2>
@@ -508,7 +468,7 @@ export default function Onboarding() {
); );
} }
function Step6() { function Step5() {
const alertCount = data.alertsCreated.length; const alertCount = data.alertsCreated.length;
const hasNotif = data.notificationConfigured; const hasNotif = data.notificationConfigured;
@@ -602,18 +562,17 @@ export default function Onboarding() {
// ── Footer navigation ───────────────────────────────────────────────────── // ── Footer navigation ─────────────────────────────────────────────────────
function Footer() { function Footer() {
if (step === 6) return null; if (step === 5) return null;
const canSkip = step === 4 || step === 5; const canSkip = step === 3 || step === 4;
const canBack = step > 1 && step !== 2; const canBack = step > 2;
async function handleNext() { async function handleNext() {
setSkipWarning(""); setSkipWarning("");
if (step === 1) await handleStep1(); if (step === 1) await handleStep1();
else if (step === 2) await handleStep2Subscribe(); else if (step === 2) await handleStep2();
else if (step === 3) await handleStep3(); else if (step === 3) await handleStep3();
else if (step === 4) await handleStep4(); else if (step === 4) await handleStep4();
else if (step === 5) await handleStep5();
} }
function handleSkip() { function handleSkip() {
@@ -628,9 +587,9 @@ export default function Onboarding() {
setStep((s) => s - 1); setStep((s) => s - 1);
} }
const nextLabel = step === 2 const nextLabel = step === 1
? "Subscribe — $1.99/mo" ? "Create Account & Subscribe"
: step === 5 : step === 4
? "Finish" ? "Finish"
: "Next →"; : "Next →";
@@ -673,12 +632,11 @@ export default function Onboarding() {
// ── Render ──────────────────────────────────────────────────────────────── // ── Render ────────────────────────────────────────────────────────────────
const stepContent = { const stepContent = {
1: <Step1 />, 1: Step1(),
2: <Step2 />, 2: Step2(),
3: <Step3 />, 3: Step3(),
4: <Step4 />, 4: Step4(),
5: <Step5 />, 5: Step5(),
6: <Step6 />,
}; };
return ( return (
@@ -686,7 +644,7 @@ export default function Onboarding() {
<div className="onboarding__container"> <div className="onboarding__container">
<h1 className="onboarding__title">Koin Ping</h1> <h1 className="onboarding__title">Koin Ping</h1>
<ProgressBar /> {ProgressBar()}
{error && ( {error && (
<div className="alert alert--error">{error}</div> <div className="alert alert--error">{error}</div>
@@ -698,7 +656,7 @@ export default function Onboarding() {
<div className="onboarding__card"> <div className="onboarding__card">
{stepContent[step]} {stepContent[step]}
<Footer /> {Footer()}
</div> </div>
{step === 1 && ( {step === 1 && (

View File

@@ -1,5 +1,5 @@
import { Navigate } from "react-router-dom"; import { Navigate } from "react-router-dom";
export default function Signup() { export default function Signup() {
return <Navigate to="/onboarding" replace />; return <Navigate to="/subscribe" replace />;
} }