Skip to content

Commit

Permalink
Stripe Connect Onboarding Integration (#528)
Browse files Browse the repository at this point in the history
* Base for stripe connect onboardnig flow

* Almost done with stripe onboarding

* Stripe Connect onboarding flow complete

* Cleanup

* More cleanup

* Improve code style/structure, improve onboarding flow, remove redundant code

* Clean up delete connect account route

* Address code review comments
  • Loading branch information
Winston-Hsiao authored Nov 7, 2024
1 parent 7684374 commit 4352a25
Show file tree
Hide file tree
Showing 16 changed files with 777 additions and 58 deletions.
17 changes: 17 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
"@react-oauth/google": "^0.12.1",
"@react-three/drei": "^9.109.2",
"@react-three/fiber": "^8.16.8",
"@stripe/connect-js": "^3.3.16",
"@stripe/react-connect-js": "^3.3.18",
"@stripe/stripe-js": "^1.54.2",
"@types/three": "^0.168.0",
"@uidotdev/usehooks": "^2.4.1",
Expand Down
16 changes: 16 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,20 @@ import Login from "@/components/pages/Login";
import Logout from "@/components/pages/Logout";
import NotFound from "@/components/pages/NotFound";
import Profile from "@/components/pages/Profile";
import SellerDashboard from "@/components/pages/SellerDashboard";
import Signup from "@/components/pages/Signup";
import { AlertQueue, AlertQueueProvider } from "@/hooks/useAlertQueue";
import { AuthenticationProvider } from "@/hooks/useAuth";

import GDPRBanner from "./components/gdpr/gdprbanner";
import DeleteConnect from "./components/pages/DeleteConnect";
import DownloadsPage from "./components/pages/Download";
import OrderSuccess from "./components/pages/OrderSuccess";
import OrdersPage from "./components/pages/Orders";
import Playground from "./components/pages/Playground";
import PrivacyPolicy from "./components/pages/PrivacyPolicy";
import ResearchPage from "./components/pages/ResearchPage";
import SellerOnboarding from "./components/pages/SellerOnboarding";
import Terminal from "./components/pages/Terminal";
import TermsOfService from "./components/pages/TermsOfService";

Expand Down Expand Up @@ -82,6 +85,19 @@ const App = () => {
<Route path="/tos" element={<TermsOfService />} />
<Route path="/privacy" element={<PrivacyPolicy />} />

<Route
path="/sell/onboarding"
element={<SellerOnboarding />}
/>
<Route
path="/sell/dashboard"
element={<SellerDashboard />}
/>
<Route
path="/delete-connect"
element={<DeleteConnect />}
/>

<Route path="/success" element={<OrderSuccess />} />
<Route path="/orders" element={<OrdersPage />} />

Expand Down
81 changes: 81 additions & 0 deletions frontend/src/components/pages/DeleteConnect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { useEffect } from "react";
import { useNavigate } from "react-router-dom";

import { useAlertQueue } from "@/hooks/useAlertQueue";
import { useAuthentication } from "@/hooks/useAuth";

export default function DeleteConnect() {
const navigate = useNavigate();
const auth = useAuthentication();
const { addErrorAlert, addAlert } = useAlertQueue();

useEffect(() => {
if (auth.isLoading) return;

if (!auth.isAuthenticated) {
navigate("/login");
return;
}

if (!auth.currentUser?.permissions?.includes("is_admin")) {
navigate("/");
return;
}
}, [auth.isLoading, auth.isAuthenticated]);

const handleDeleteTestAccounts = async () => {
try {
const { data, error } = await auth.client.POST(
"/stripe/connect/delete/accounts",
{},
);

if (error) {
addErrorAlert(error);
return;
}

addAlert(`Successfully deleted ${data.count} test accounts`, "success");
setTimeout(() => {
navigate("/sell/onboarding");
}, 2000);
} catch (error) {
addErrorAlert(`Failed to delete test accounts: ${error}`);
}
};

if (auth.isLoading) {
return (
<div className="container mx-auto px-4 py-8">
<div className="max-w-2xl mx-auto">
<p>Loading...</p>
</div>
</div>
);
}

return (
<div className="container mx-auto px-4 py-8">
<div className="max-w-2xl mx-auto">
<h1 className="text-3xl font-bold mb-6">
Delete Test Connect Accounts
</h1>

<div className="bg-red-50 p-6 rounded-lg mb-6">
<h2 className="text-red-800 font-semibold mb-2">⚠️ Warning</h2>
<p className="text-red-700 mb-4">
This action will delete all test Stripe Connect accounts associated
with this environment. This operation cannot be undone.
</p>
</div>

<button
onClick={handleDeleteTestAccounts}
className="w-full bg-red-600 text-white px-6 py-3 rounded-lg hover:bg-red-700"
>
Delete All Test Connect Accounts
</button>
</div>
</div>
);
}
68 changes: 56 additions & 12 deletions frontend/src/components/pages/Profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import UpvotedGrid from "@/components/listings/UpvotedGrid";
import { Card, CardContent, CardHeader } from "@/components/ui/Card";
import { Input, TextArea } from "@/components/ui/Input/Input";
import Spinner from "@/components/ui/Spinner";
import { Tooltip } from "@/components/ui/ToolTip";
import { Button } from "@/components/ui/button";
import { paths } from "@/gen/api";
import { useAlertQueue } from "@/hooks/useAlertQueue";
Expand Down Expand Up @@ -187,15 +188,16 @@ export const RenderProfile = (props: RenderProfileProps) => {
<div className="space-y-8 mb-12">
<Card className="w-full max-w-4xl mx-auto">
<CardHeader className="flex flex-col items-center space-y-4">
<div className="flex flex-col items-center space-y-2 mb-2">
<div className="flex flex-col items-center space-y-2">
<h1 className="text-3xl font-bold text-primary-9">
{user.first_name || user.last_name
? `${user.first_name || ""} ${user.last_name || ""}`
: "Anonymous Creator"}
</h1>
<div className="flex gap-2">
<p className="text-sm text-gray-1 bg-gray-10 px-3 py-1 rounded-md">
@{user.username}
<span className="font-semibold mr-0.5 select-none">@</span>
{user.username}
</p>
{user.permissions && (
<p className="text-sm text-primary-9 bg-primary-3 px-3 py-1 rounded-md">
Expand All @@ -211,13 +213,10 @@ export const RenderProfile = (props: RenderProfileProps) => {
</p>
</div>
{!isEditing && canEdit && (
<div className="flex space-x-2">
<div className="flex flex-wrap gap-2 justify-center">
<Button onClick={() => navigate("/keys")} variant="primary">
API Keys
</Button>
<Button onClick={() => navigate("/orders")} variant="default">
Orders
</Button>
<Button onClick={() => setIsEditing(true)} variant="outline">
Edit Profile
</Button>
Expand Down Expand Up @@ -358,11 +357,11 @@ export const RenderProfile = (props: RenderProfileProps) => {
) : (
<div className="space-y-6">
<div>
<h2 className="text-xl font-semibold mb-2">Bio</h2>
<h2 className="text-lg font-medium mb-2">Bio</h2>
{user.bio ? (
<p>{user.bio}</p>
) : (
<p className="text-gray-11">
<p className="text-gray-11 text-sm">
No bio set. Edit your profile to add a bio.
</p>
)}
Expand All @@ -374,9 +373,54 @@ export const RenderProfile = (props: RenderProfileProps) => {

<Card className="w-full max-w-4xl mx-auto">
<CardHeader>
<h2 className="text-2xl font-bold">Listings</h2>
<h2 className="text-2xl font-bold">Store</h2>
</CardHeader>
<CardContent>
<div className="mb-4">
{user.stripe_connect_account_id &&
!user.stripe_connect_onboarding_completed ? (
<p className="text-gray-11 text-sm">
Your Stripe account setup is not complete. Please resolve
outstanding requirements to begin selling robots. It may take
some time for Stripe to process your info between submissions.
</p>
) : user.stripe_connect_onboarding_completed ? (
<p className="text-gray-11 text-sm">
Stripe account setup complete.
</p>
) : null}
</div>
<div className="flex gap-2">
<Button onClick={() => navigate("/orders")} variant="primary">
Orders
</Button>
{!user.stripe_connect_account_id ? (
<Tooltip content="Start seller onboarding" position="bottom">
<Button
onClick={() => navigate("/sell/onboarding")}
variant="outline"
>
Sell Robots
</Button>
</Tooltip>
) : !user.stripe_connect_onboarding_completed ? (
<Tooltip content="Continue seller onboarding" position="bottom">
<Button
onClick={() => navigate("/sell/onboarding")}
variant="outline"
>
Complete Seller Setup
</Button>
</Tooltip>
) : (
<Button
onClick={() => navigate("/sell/dashboard")}
variant="outline"
>
Seller Dashboard
</Button>
)}
</div>
<div className="flex flex-col items-center space-y-4">
<Tabs
defaultValue="own"
Expand All @@ -386,13 +430,13 @@ export const RenderProfile = (props: RenderProfileProps) => {
<TabsList className="flex justify-center space-x-4 mb-4">
<TabsTrigger
value="own"
className="px-3 py-1.5 rounded-md transition-colors duration-300 hover:bg-gray-11 hover:text-gray-1 data-[state=active]:bg-primary-9 data-[state=active]:text-gray-1"
className="text-sm px-3 py-1.5 rounded-md transition-colors duration-300 hover:bg-gray-9 hover:text-gray-1 data-[state=active]:bg-gray-12 data-[state=active]:text-gray-1"
>
Overview
Your Listings
</TabsTrigger>
<TabsTrigger
value="upvoted"
className="px-3 py-1.5 rounded-md transition-colors duration-300 hover:bg-gray-11 hover:text-gray-1 data-[state=active]:bg-primary-9 data-[state=active]:text-gray-1"
className="text-sm px-3 py-1.5 rounded-md transition-colors duration-300 hover:bg-gray-9 hover:text-gray-1 data-[state=active]:bg-gray-12 data-[state=active]:text-gray-1"
>
Upvoted
</TabsTrigger>
Expand Down
69 changes: 69 additions & 0 deletions frontend/src/components/pages/SellerDashboard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { useEffect } from "react";
import { useNavigate } from "react-router-dom";

import { useAuthentication } from "@/hooks/useAuth";
import { Check } from "lucide-react";

export default function SellerDashboard() {
const navigate = useNavigate();
const auth = useAuthentication();

useEffect(() => {
auth.fetchCurrentUser();
}, []);

useEffect(() => {
if (auth.isLoading) return;

if (!auth.isAuthenticated) {
navigate("/login");
return;
}

// Redirect to onboarding if not completed
if (!auth.currentUser?.stripe_connect_onboarding_completed) {
navigate("/sell/onboarding");
return;
}
}, [auth.isLoading, auth.isAuthenticated, auth.currentUser]);

if (auth.isLoading) {
return (
<div className="container mx-auto px-4 py-8">
<div className="max-w-2xl mx-auto">
<p>Loading...</p>
</div>
</div>
);
}

return (
<div className="container mx-auto px-4 py-8">
<div className="max-w-2xl mx-auto">
<h1 className="text-3xl font-bold mb-6">Seller Dashboard</h1>

<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-xl font-semibold mb-4">Account Status</h2>
<div className="flex gap-2 text-green-600">
<Check />
<p>
Your K-Scale seller account is active and ready to receive
payments.
</p>
</div>

<div className="mt-6">
<a
href={`https://dashboard.stripe.com/${auth.currentUser?.stripe_connect_account_id}`}
target="_blank"
rel="noopener noreferrer"
className="bg-primary-9 text-white px-6 py-3 rounded-lg hover:bg-primary-9/80 inline-block"
>
Open Stripe Dashboard
</a>
</div>
</div>
</div>
</div>
);
}
Loading

0 comments on commit 4352a25

Please sign in to comment.