Files
Shifted/app/connect/page.tsx
2026-02-10 01:14:19 +00:00

348 lines
11 KiB
TypeScript

"use client";
import { Suspense, useEffect, useMemo, useState } from "react";
import { useSearchParams } from "next/navigation";
import { signIn, signOut, useSession } from "next-auth/react";
type AccountStatus = {
readyToProcessPayments: boolean;
onboardingComplete: boolean;
requirementsStatus?: string;
};
function ConnectDashboardClient() {
const [displayName, setDisplayName] = useState(
process.env.NEXT_PUBLIC_STORE_DEFAULT_NAME || ""
);
const [contactEmail, setContactEmail] = useState("");
const [accountId, setAccountId] = useState("");
const [storeSlug, setStoreSlug] = useState("storeshifted");
const [status, setStatus] = useState<AccountStatus | null>(null);
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState("");
const [productName, setProductName] = useState("");
const [productDescription, setProductDescription] = useState("");
const [productPrice, setProductPrice] = useState("4999");
const [currency, setCurrency] = useState("usd");
const searchParams = useSearchParams();
const { data: session, status: sessionStatus } = useSession();
const isAuthenticated = sessionStatus === "authenticated";
useEffect(() => {
const stored = window.localStorage.getItem("connectAccountId");
if (stored) setAccountId(stored);
const storedSlug = window.localStorage.getItem("connectStoreSlug");
if (storedSlug) setStoreSlug(storedSlug);
}, []);
useEffect(() => {
const fromQuery = searchParams.get("accountId");
if (fromQuery) setAccountId(fromQuery);
}, [searchParams]);
useEffect(() => {
async function loadStore() {
if (!isAuthenticated) return;
try {
const res = await fetch("/api/store/me");
const data = await res.json();
if (res.ok && data.slug) {
setStoreSlug(data.slug);
if (data.stripeAccountId) setAccountId(data.stripeAccountId);
}
} catch {
// ignore
}
}
loadStore();
}, [session]);
useEffect(() => {
if (accountId) {
window.localStorage.setItem("connectAccountId", accountId);
}
}, [accountId]);
useEffect(() => {
if (storeSlug) {
window.localStorage.setItem("connectStoreSlug", storeSlug);
}
}, [storeSlug]);
const storefrontUrl = useMemo(() => {
if (!storeSlug) return "";
return `/storefront/${storeSlug}`;
}, [storeSlug]);
async function createAccount() {
if (!isAuthenticated) {
setMessage("Please sign in first.");
return;
}
setMessage("");
setLoading(true);
try {
const res = await fetch("/api/connect/account/create", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ displayName, contactEmail, slug: storeSlug }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || "Failed to create account");
setAccountId(data.accountId);
setMessage("Connected account created.");
} catch (err: any) {
setMessage(err.message || "Failed to create account.");
} finally {
setLoading(false);
}
}
async function refreshStatus() {
if (!accountId) return;
setMessage("");
setLoading(true);
try {
const res = await fetch(`/api/connect/account/status?accountId=${accountId}`);
const data = await res.json();
if (!res.ok) throw new Error(data.error || "Failed to fetch status");
setStatus(data);
} catch (err: any) {
setMessage(err.message || "Failed to fetch status.");
} finally {
setLoading(false);
}
}
async function startOnboarding() {
if (!accountId) return;
setMessage("");
setLoading(true);
try {
const res = await fetch("/api/connect/account/link", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ accountId }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || "Failed to create account link");
window.location.href = data.url;
} catch (err: any) {
setMessage(err.message || "Failed to start onboarding.");
} finally {
setLoading(false);
}
}
async function createProduct() {
if (!accountId) return;
setMessage("");
setLoading(true);
try {
const res = await fetch("/api/connect/products/create", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
accountId,
name: productName,
description: productDescription,
priceInCents: Number(productPrice),
currency,
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || "Failed to create product");
setMessage(`Product created: ${data.productId}`);
} catch (err: any) {
setMessage(err.message || "Failed to create product.");
} finally {
setLoading(false);
}
}
async function createSubscription() {
if (!accountId) return;
setMessage("");
setLoading(true);
try {
const res = await fetch("/api/connect/subscription/create", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ accountId }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || "Failed to create subscription");
window.location.href = data.url;
} catch (err: any) {
setMessage(err.message || "Failed to create subscription.");
} finally {
setLoading(false);
}
}
async function openBillingPortal() {
if (!accountId) return;
setMessage("");
setLoading(true);
try {
const res = await fetch("/api/connect/subscription/portal", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ accountId }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || "Failed to create portal session");
window.location.href = data.url;
} catch (err: any) {
setMessage(err.message || "Failed to open billing portal.");
} finally {
setLoading(false);
}
}
return (
<main className="connect-page">
<section className="section">
<div className="container connect-page__inner">
<h1 className="page-title">Stripe Connect Demo</h1>
<p className="connect-page__subtitle">
This demo onboards connected accounts, creates products on their
account, and provides a storefront for customers to purchase.
</p>
<div className="connect-card">
<h2>Account Access</h2>
{isAuthenticated ? (
<div className="connect-actions">
<div className="connect-muted">Signed in as {session!.user.email}</div>
<button className="btn btn--ghost" onClick={() => signOut()}>
Sign out
</button>
</div>
) : (
<div className="connect-actions">
<div className="connect-muted">Sign in to create an account.</div>
<button className="btn" onClick={() => signIn()}>
Sign in
</button>
</div>
)}
</div>
<div className="connect-card">
<h2>Create Connected Account</h2>
<div className="connect-form">
<input
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
placeholder="Display name"
/>
<input
value={contactEmail}
onChange={(e) => setContactEmail(e.target.value)}
placeholder="Contact email"
/>
<input
value={storeSlug}
onChange={(e) => setStoreSlug(e.target.value)}
placeholder="Store slug (e.g. storeshifted)"
/>
<button className="btn" onClick={createAccount} disabled={loading}>
Create Account
</button>
</div>
{accountId ? (
<div className="connect-muted">Connected account: {accountId}</div>
) : null}
</div>
<div className="connect-card">
<h2>Onboard to Collect Payments</h2>
<div className="connect-actions">
<button className="btn" onClick={startOnboarding} disabled={loading}>
Onboard to collect payments
</button>
<button className="btn" onClick={refreshStatus} disabled={loading}>
Refresh status
</button>
</div>
{status ? (
<div className="connect-status">
<div>Ready to process payments: {String(status.readyToProcessPayments)}</div>
<div>Onboarding complete: {String(status.onboardingComplete)}</div>
<div>Requirements status: {status.requirementsStatus || "unknown"}</div>
</div>
) : null}
</div>
<div className="connect-card">
<h2>Create Product on Connected Account</h2>
<div className="connect-form">
<input
value={productName}
onChange={(e) => setProductName(e.target.value)}
placeholder="Product name"
/>
<input
value={productDescription}
onChange={(e) => setProductDescription(e.target.value)}
placeholder="Description"
/>
<input
value={productPrice}
onChange={(e) => setProductPrice(e.target.value)}
placeholder="Price in cents"
/>
<input
value={currency}
onChange={(e) => setCurrency(e.target.value)}
placeholder="Currency (usd)"
/>
<button className="btn" onClick={createProduct} disabled={loading}>
Create Product
</button>
</div>
</div>
<div className="connect-card">
<h2>Storefront</h2>
<p className="connect-muted">
This demo uses the connected account ID in the URL. Replace this
with your own identifier in production.
</p>
<div className="connect-actions">
<a className="btn" href={storefrontUrl || "#"}>Open Storefront</a>
</div>
</div>
<div className="connect-card">
<h2>Subscription (Platform Billing)</h2>
<p className="connect-muted">
Creates a hosted subscription checkout session and a billing portal
for the connected account.
</p>
<div className="connect-actions">
<button className="btn" onClick={createSubscription} disabled={loading}>
Subscribe
</button>
<button className="btn" onClick={openBillingPortal} disabled={loading}>
Open Billing Portal
</button>
</div>
</div>
{message ? <div className="connect-message">{message}</div> : null}
</div>
</section>
</main>
);
}
export default function ConnectDashboardPage() {
return (
<Suspense fallback={null}>
<ConnectDashboardClient />
</Suspense>
);
}