Skip to content

Commit

Permalink
feat: add button to get more cshots (#19)
Browse files Browse the repository at this point in the history
  • Loading branch information
baptadn authored Dec 16, 2022
1 parent 1574b4b commit c3b6853
Show file tree
Hide file tree
Showing 12 changed files with 271 additions and 29 deletions.
13 changes: 13 additions & 0 deletions prisma/migrations/20221216182001_payments/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
-- CreateTable
CREATE TABLE "payments" (
"id" TEXT NOT NULL,
"type" TEXT NOT NULL,
"status" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"projectId" TEXT,

CONSTRAINT "payments_pkey" PRIMARY KEY ("id")
);

-- AddForeignKey
ALTER TABLE "payments" ADD CONSTRAINT "payments_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE SET NULL ON UPDATE CASCADE;
8 changes: 8 additions & 0 deletions prisma/migrations/20221216182142_stripe_session/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
Warnings:
- Added the required column `stripeSessionId` to the `payments` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "payments" ADD COLUMN "stripeSessionId" TEXT NOT NULL;
23 changes: 18 additions & 5 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ model VerificationToken {
}

model Project {
id String @id @default(cuid())
id String @id @default(cuid())
name String
replicateModelId String?
stripePaymentId String?
Expand All @@ -70,12 +70,13 @@ model Project {
instanceClass String
imageUrls String[]
zipImageUrl String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
User User? @relation(fields: [userId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
User User? @relation(fields: [userId], references: [id])
userId String?
shots Shot[]
credits Int @default(100)
credits Int @default(100)
Payment Payment[]
}

model Shot {
Expand All @@ -91,3 +92,15 @@ model Shot {
bookmarked Boolean? @default(false)
blurhash String?
}

model Payment {
id String @id @default(cuid())
type String
status String
stripeSessionId String
createdAt DateTime @default(now())
Project Project? @relation(fields: [projectId], references: [id])
projectId String?
@@map("payments")
}
1 change: 0 additions & 1 deletion src/components/layout/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
HStack,
Icon,
IconButton,
Popover,
Text,
Tooltip,
} from "@chakra-ui/react";
Expand Down
3 changes: 2 additions & 1 deletion src/components/projects/FormPayment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ const FormPayment = ({

useQuery(
"check-payment",
() => axios.get(`/api/checkout/check/${query.ppi}/${query.session_id}`),
() =>
axios.get(`/api/checkout/check/${query.ppi}/${query.session_id}/studio`),
{
cacheTime: 0,
refetchInterval: 10,
Expand Down
104 changes: 104 additions & 0 deletions src/components/projects/shot/BuyShotButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import React, { useEffect, useState } from "react";
import {
Menu,
MenuButton,
MenuList,
MenuItem,
Button,
HStack,
Text,
} from "@chakra-ui/react";
import { useRouter } from "next/router";
import { useQuery } from "react-query";
import axios from "axios";
import { BsChevronDown } from "react-icons/bs";
import { IoIosFlash } from "react-icons/io";

const BuyShotButton = ({
credits,
onPaymentSuccess,
}: {
credits: number;
onPaymentSuccess: (credits: number) => void;
}) => {
const { push, query } = useRouter();
const [waitingPayment, setWaitingPayment] = useState(false);

const { isLoading } = useQuery(
"check-shot-payment",
() =>
axios.get(`/api/checkout/check/${query.ppi}/${query.session_id}/shot`),
{
cacheTime: 0,
refetchInterval: 4,
retry: 0,
enabled: waitingPayment,
onSuccess: (response) => {
onPaymentSuccess(response.data.credits);
},
onSettled: () => {
setWaitingPayment(false);
},
}
);

useEffect(() => {
setWaitingPayment(query.ppi === query.id);
}, [query]);

const handleShotPayment = (quantity: number) => {
push(`/api/checkout/shots?quantity=${quantity}&ppi=${query.id}`);
};

return (
<Menu>
<MenuButton
rightIcon={<BsChevronDown />}
isLoading={isLoading}
size="xs"
shadow="none"
variant="brand"
as={Button}
>
<HStack spacing={0}>
<IoIosFlash />
{credits === 0 ? (
<Text>Buy more shots</Text>
) : (
<Text>
{credits} Shot{credits > 1 && "s"} left
</Text>
)}
</HStack>
</MenuButton>
<MenuList fontSize="sm">
<MenuItem
command="$4"
onClick={() => {
handleShotPayment(100);
}}
>
Add <b>100 shots</b>
</MenuItem>
<MenuItem
command="$7"
onClick={() => {
handleShotPayment(200);
}}
>
Add <b>200 shots</b>
</MenuItem>
<MenuItem
command="$9"
onClick={() => {
handleShotPayment(300);
}}
>
Add <b>300 shots</b>
</MenuItem>
</MenuList>
</Menu>
);
};

export default BuyShotButton;
56 changes: 56 additions & 0 deletions src/pages/api/checkout/check/[ppi]/[sessionId]/shot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { NextApiRequest, NextApiResponse } from "next";
import Stripe from "stripe";
import db from "@/core/db";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2022-11-15",
});

export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const sessionId = req.query.sessionId as string;
const ppi = req.query.ppi as string;

const session = await stripe.checkout.sessions.retrieve(sessionId);

const payments = await db.payment.findMany({
where: {
stripeSessionId: sessionId,
projectId: ppi,
status: "paid",
type: "credits",
},
});

if (payments.length > 0) {
return res
.status(400)
.json({ success: false, error: "payment_already_processed" });
}

if (
session.payment_status === "paid" &&
session.metadata?.projectId === ppi
) {
const quantity = Number(session.metadata?.quantity);
const project = await db.project.update({
where: { id: ppi },
data: { credits: { increment: quantity } },
});

await db.payment.create({
data: {
status: "paid",
projectId: ppi,
type: "credits",
stripeSessionId: sessionId,
},
});

return res.status(200).json({ success: true, credits: project.credits });
}

return res.status(400).json({ success: false });
}
2 changes: 0 additions & 2 deletions src/pages/api/checkout/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,4 @@ export default async function handler(
} catch (err: any) {
return res.status(400).json(err.message);
}

return res.status(400).json({});
}
49 changes: 49 additions & 0 deletions src/pages/api/checkout/shots.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { NextApiRequest, NextApiResponse } from "next";
import Stripe from "stripe";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2022-11-15",
});

const PRICES = { 100: 400, 200: 700, 300: 900 };

export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const quantity = Number(req.query.quantity);
const ppi = req.query.ppi;

if (quantity !== 100 && quantity !== 200 && quantity !== 300) {
return res.status(400).json("invalid_quantity");
}

try {
const session = await stripe.checkout.sessions.create({
allow_promotion_codes: true,
metadata: {
projectId: req.query.ppi as string,
quantity,
},
line_items: [
{
price_data: {
currency: "usd",
unit_amount: PRICES[quantity],
product_data: {
name: `⚡️ Refill +${quantity} shots`,
},
},
quantity: 1,
},
],
mode: "payment",
success_url: `${process.env.NEXTAUTH_URL}/studio/${ppi}/?session_id={CHECKOUT_SESSION_ID}&ppi=${ppi}`,
cancel_url: `${process.env.NEXTAUTH_URL}/studio/${ppi}`,
});

return res.redirect(303, session.url!);
} catch (err: any) {
return res.status(400).json(err.message);
}
}
4 changes: 3 additions & 1 deletion src/pages/api/projects/[id]/predictions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const { data } = await replicateClient.post(
`https://api.replicate.com/v1/predictions`,
{
input: { prompt },
input: {
prompt,
},
version: project.modelVersionId,
}
);
Expand Down
Loading

1 comment on commit c3b6853

@vercel
Copy link

@vercel vercel bot commented on c3b6853 Dec 16, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.