Skip to content

Commit

Permalink
Stripe Connect Listing Payments (#568)
Browse files Browse the repository at this point in the history
* Base for stripe connect listing integration

* Fixed onboarding flow

* Improve onboarding flow and adjust to new styling

* Updated styling for bot listing flow

* Checkout from listing page working

* Updated orders to work wiht new stripe listings
  • Loading branch information
Winston-Hsiao authored Nov 10, 2024
1 parent 3748c37 commit 20043e6
Show file tree
Hide file tree
Showing 28 changed files with 1,676 additions and 394 deletions.
18 changes: 12 additions & 6 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import APIKeys from "@/components/pages/APIKeys";
import About from "@/components/pages/About";
import Account from "@/components/pages/Account";
import Browse from "@/components/pages/Browse";
import Create from "@/components/pages/Create";
import CreateSell from "@/components/pages/CreateSell";
import CreateShare from "@/components/pages/CreateShare";
import DeleteConnect from "@/components/pages/DeleteConnect";
import EmailSignup from "@/components/pages/EmailSignup";
import FileBrowser from "@/components/pages/FileBrowser";
Expand Down Expand Up @@ -90,14 +91,19 @@ const App = () => {
/>

{/* Listings */}
<Route path={"/create"} element={<CreateSell />} />
<Route path={ROUTES.BOTS.path}>
<Route
path={ROUTES.BOTS.$.BROWSE.relativePath}
element={<Browse />}
/>
<Route
path={ROUTES.BOTS.$.CREATE.relativePath}
element={<Create />}
element={<CreateShare />}
/>
<Route
path={ROUTES.BOTS.$.SELL.relativePath}
element={<CreateSell />}
/>
</Route>
<Route path={ROUTES.BOT.path} element={<Listing />} />
Expand All @@ -108,14 +114,14 @@ const App = () => {

{/* Seller */}
<Route path={ROUTES.SELL.path}>
<Route
path={ROUTES.SELL.$.DASHBOARD.relativePath}
element={<SellerDashboard />}
/>
<Route
path={ROUTES.SELL.$.ONBOARDING.relativePath}
element={<SellerOnboarding />}
/>
<Route
path={ROUTES.SELL.$.DASHBOARD.relativePath}
element={<SellerDashboard />}
/>
<Route
path={ROUTES.SELL.$.DELETE.relativePath}
element={<DeleteConnect />}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/Drawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export const Drawer = ({
transition={{
ease: "easeInOut",
}}
className="absolute bottom-0 h-[75vh] w-full overflow-hidden rounded-t-3xl bg-neutral-900"
className="absolute bottom-0 h-[60vh] w-full overflow-hidden rounded-t-3xl bg-neutral-900"
style={{ y }}
drag="y"
dragControls={controls}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/gdpr/gdprbanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ const GDPRBanner: React.FC = () => {
Opt out
</button>
<button
className="bg-primary-9 text-white rounded-full px-4 py-2 transition-colors duration-300 hover:bg-primary-8"
className="bg-gray-1 text-gray-12 rounded-full px-4 py-2 transition-colors duration-300 hover:bg-gray-11"
onClick={handleAccept}
>
Accept
Expand Down
69 changes: 69 additions & 0 deletions frontend/src/components/listing/ListingPayment.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import CheckoutButton from "@/components/stripe/CheckoutButton";
import { formatPrice } from "@/lib/utils/formatNumber";

interface Props {
listingId: string;
stripeProductId: string;
priceAmount: number;
inventoryType: "finite" | "infinite" | "preorder";
inventoryQuantity?: number;
preorderReleaseDate?: number;
isReservation: boolean;
reservationDepositAmount?: number;
}

const ListingPayment = ({
listingId,
stripeProductId,
priceAmount,
inventoryType,
inventoryQuantity,
preorderReleaseDate,
isReservation,
reservationDepositAmount,
}: Props) => {
return (
<div className="p-4">
<h3 className="text-lg font-semibold mb-2">Purchase Information</h3>

<div className="space-y-2 mb-4">
<div className="flex justify-between">
<span>Price:</span>
<span className="font-semibold">{formatPrice(priceAmount)}</span>
</div>

{isReservation && reservationDepositAmount && (
<div className="flex justify-between text-sm text-gray-2">
<span>Reservation Deposit:</span>
<span>{formatPrice(reservationDepositAmount)}</span>
</div>
)}

{inventoryType === "finite" && inventoryQuantity !== undefined && (
<div className="flex justify-between text-sm text-gray-2">
<span>Available Units:</span>
<span>{inventoryQuantity}</span>
</div>
)}

{inventoryType === "preorder" && preorderReleaseDate && (
<div className="flex justify-between text-sm text-gray-2">
<span>Release Date:</span>
<span>
{new Date(preorderReleaseDate * 1000).toLocaleDateString()}
</span>
</div>
)}
</div>
<div className="flex justify-end">
<CheckoutButton
listingId={listingId}
stripeProductId={stripeProductId}
label="Purchase Now"
/>
</div>
</div>
);
};

export default ListingPayment;
30 changes: 29 additions & 1 deletion frontend/src/components/listing/ListingRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import ListingImageGallery from "@/components/listing/ListingImageGallery";
import ListingMetadata from "@/components/listing/ListingMetadata";
import ListingName from "@/components/listing/ListingName";
import ListingOnshape from "@/components/listing/ListingOnshape";
import ListingPayment from "@/components/listing/ListingPayment";
import ListingRegisterRobot from "@/components/listing/ListingRegisterRobot";
import { ListingResponse } from "@/components/listing/types";

Expand All @@ -27,9 +28,17 @@ const ListingRenderer = ({ listing }: { listing: ListingResponse }) => {
user_vote: userVote,
onshape_url: onshapeUrl,
is_featured: isFeatured,
stripe_product_id: stripeProductId,
price_amount: priceAmount,
inventory_type: inventoryType,
inventory_quantity: inventoryQuantity,
preorder_release_date: preorderReleaseDate,
is_reservation: isReservation,
reservation_deposit_amount: reservationDepositAmount,
} = listing;
const [artifacts, setArtifacts] = useState(initialArtifacts);
const [currentImageIndex, setCurrentImageIndex] = useState(0);
const isForSale = priceAmount && stripeProductId && inventoryType;

return (
<div className="max-w-6xl mx-auto p-4 pt-12">
Expand Down Expand Up @@ -61,7 +70,26 @@ const ListingRenderer = ({ listing }: { listing: ListingResponse }) => {
userVote={userVote}
/>

<hr className="border-gray-200 my-4" />
{/* Add payment section if price exists */}
{isForSale && (
<>
<hr className="border-gray-2 my-4" />
<ListingPayment
listingId={listingId}
stripeProductId={stripeProductId}
priceAmount={priceAmount}
inventoryType={
inventoryType as "finite" | "infinite" | "preorder"
}
inventoryQuantity={inventoryQuantity || undefined}
preorderReleaseDate={preorderReleaseDate || undefined}
isReservation={isReservation || false}
reservationDepositAmount={reservationDepositAmount || undefined}
/>
</>
)}

<hr className="border-gray-2 my-4" />

{/* Build this robot */}
<div className="flex items-baseline gap-4">
Expand Down
16 changes: 9 additions & 7 deletions frontend/src/components/listing/UploadContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ const UploadContent: FC<UploadContentProps> = ({ images, onChange }) => {
}, [images]);

return (
<div className="p-6 bg-black border border-gray-500 rounded-lg">
<div className="p-6 bg-gray-12 border border-gray-11 rounded-lg">
<ImageUploading
multiple
value={images}
Expand All @@ -65,15 +65,17 @@ const UploadContent: FC<UploadContentProps> = ({ images, onChange }) => {
<div className="upload__image-wrapper">
{/* Dropzone Area */}
<div
className={`border-2 border-dashed p-5 rounded-lg flex flex-col items-center justify-center h-64 transition-colors duration-300 ${
isDragging
? "border-orange-900 bg-orange-300"
: "border-gray-6 bg-gray-900"
}`}
className={`
border-2 border-dashed p-5 rounded-lg flex flex-col items-center justify-center h-64 transition-colors duration-300 hover:cursor-pointer hover:bg-gray-10 hover:border-gray-1
${
isDragging
? "border-gray-1 bg-gray-10"
: "border-gray-5 bg-gray-11"
}`}
onClick={onImageUpload}
{...dragProps}
>
<p className="text-gray-300">
<p className="text-gray-1">
Drag & drop images here, click to select files, or paste an
image from your clipboard
</p>
Expand Down
77 changes: 77 additions & 0 deletions frontend/src/components/modals/CreateListingModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { useNavigate } from "react-router-dom";

import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { useAuthentication } from "@/hooks/useAuth";
import ROUTES from "@/lib/types/routes";
import { Share2, ShoppingBag } from "lucide-react";

interface Props {
isOpen: boolean;
onOpenChange: (open: boolean) => void;
}

export const CreateListingModal = ({ isOpen, onOpenChange }: Props) => {
const navigate = useNavigate();
const auth = useAuthentication();
const canSell = auth.currentUser?.stripe_connect_onboarding_completed;

const handleOptionClick = (path: string) => {
onOpenChange(false);
navigate(path);
};

return (
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[600px] bg-gray-12 text-gray-1 border border-gray-1 rounded-lg shadow-lg">
<DialogHeader>
<DialogTitle>What would you like to do?</DialogTitle>
</DialogHeader>
<div className="grid grid-cols-2 gap-4 p-4">
<div
onClick={() =>
handleOptionClick(
`${ROUTES.BOTS.path}/${ROUTES.BOTS.$.CREATE.relativePath}`,
)
}
className="flex flex-col items-center justify-center p-6 rounded-lg border border-gray-7 hover:border-gray-1 hover:bg-gray-11 cursor-pointer transition-all"
>
<Share2 className="w-12 h-12 mb-4" />
<h3 className="text-lg font-semibold">Share a Robot</h3>
<p className="text-sm text-gray-7 text-center mt-2">
Share your Robot with the community
</p>
</div>

<div
onClick={() =>
canSell &&
handleOptionClick(
`${ROUTES.BOTS.path}/${ROUTES.BOTS.$.SELL.relativePath}`,
)
}
className={`flex flex-col items-center justify-center p-6 rounded-lg border ${
canSell
? "border-gray-7 hover:border-gray-1 hover:bg-gray-11 cursor-pointer"
: "border-gray-4 opacity-50 cursor-not-allowed"
} transition-all`}
>
<ShoppingBag className="w-12 h-12 mb-4" />
<h3 className="text-lg font-semibold">Sell a Robot</h3>
<p className="text-sm text-gray-7 text-center mt-2">
{canSell
? "List your Robot for sale"
: "Complete Seller onboarding to sell"}
</p>
</div>
</div>
</DialogContent>
</Dialog>
);
};

export default CreateListingModal;
12 changes: 6 additions & 6 deletions frontend/src/components/orders/OrderCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,14 @@ const OrderCard: React.FC<{ orderWithProduct: OrderWithProduct }> = ({
const getStatusColor = (status: string) => {
if (isRedStatus) return "bg-red-500";
if (activeStatuses.includes(status) || status === "delivered")
return "bg-primary-9";
return "bg-primary";
return "bg-gray-300";
};

const getTextColor = (status: string) => {
if (isRedStatus) return "text-red-600";
if (activeStatuses.includes(status) || status === "delivered")
return "text-primary-9";
return "text-primary";
return "text-gray-600";
};

Expand All @@ -78,7 +78,7 @@ const OrderCard: React.FC<{ orderWithProduct: OrderWithProduct }> = ({
};

return (
<div className="bg-white shadow-md rounded-lg p-4 md:p-6 w-full">
<div className="bg-gray-1 shadow-md rounded-lg p-4 md:p-6 w-full">
<h2 className="text-gray-12 font-bold text-2xl mb-1">{product.name}</h2>
<p className="text-gray-11 mb-2 sm:text-lg">
Status:{" "}
Expand Down Expand Up @@ -138,7 +138,7 @@ const OrderCard: React.FC<{ orderWithProduct: OrderWithProduct }> = ({
index <= currentStatusIndex
? getStatusColor(status)
: "bg-gray-300"
} text-white`}
} text-gray-12`}
>
{index < currentStatusIndex ? (
<svg
Expand Down Expand Up @@ -199,8 +199,8 @@ const OrderCard: React.FC<{ orderWithProduct: OrderWithProduct }> = ({
</p>
)}

<div className="mt-4 text-sm bg-gray-3 p-3 rounded-md">
<h3 className="text-gray-12 font-semibold text-lg">Shipping Address</h3>
<div className="mt-4 text-sm bg-gray-3 p-3 rounded-md text-gray-12">
<h3 className="font-semibold text-lg">Shipping Address</h3>
<p>{order.shipping_name}</p>
<p>{order.shipping_address_line1}</p>
{order.shipping_address_line2 && <p>{order.shipping_address_line2}</p>}
Expand Down
Loading

0 comments on commit 20043e6

Please sign in to comment.