From faaba096230d5c4f4ef990805cb0d0cd46f738d3 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Thu, 16 Apr 2026 11:32:37 +0700 Subject: [PATCH] purchase credits only for logged in users --- .../src/components/credits-modal.tsx | 116 ++++++++++++++++++ .../pricing-table/pricing-table.tsx | 2 +- .../src/pages/pricing/pricing.tsx | 57 ++++++++- frontend/libs/api/src/lib/BillingApi.ts | 35 ++++++ 4 files changed, 206 insertions(+), 4 deletions(-) create mode 100644 frontend/apps/artcraft-website/src/components/credits-modal.tsx diff --git a/frontend/apps/artcraft-website/src/components/credits-modal.tsx b/frontend/apps/artcraft-website/src/components/credits-modal.tsx new file mode 100644 index 0000000000..85a168c6a8 --- /dev/null +++ b/frontend/apps/artcraft-website/src/components/credits-modal.tsx @@ -0,0 +1,116 @@ +import { useState } from "react"; +import { Modal } from "@storyteller/ui-modal"; +import { Button } from "@storyteller/ui-button"; +import { faCoins } from "@fortawesome/pro-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { BillingApi } from "@storyteller/api"; +import { toast } from "@storyteller/ui-toaster"; + +interface CreditPack { + id: string; + total: number; + priceUsd: number; + badge?: string; +} + +const CREDIT_PACKS: CreditPack[] = [ + { id: "artcraft_1000", total: 1000, priceUsd: 10 }, + { id: "artcraft_2500", total: 2500, priceUsd: 25 }, + { id: "artcraft_5000", total: 5000, priceUsd: 50, badge: "Popular" }, + { id: "artcraft_10000", total: 10000, priceUsd: 100 }, +]; + +interface CreditsModalProps { + isOpen: boolean; + onClose: () => void; +} + +export function CreditsModal({ isOpen, onClose }: CreditsModalProps) { + const [purchasingId, setPurchasingId] = useState(null); + + const handlePurchase = async (pack: CreditPack) => { + setPurchasingId(pack.id); + try { + const api = new BillingApi(); + const response = await api.CreditsPackCheckout({ + creditsPack: pack.id, + }); + + if (response.success && response.data?.stripeCheckoutRedirectUrl) { + window.location.href = response.data.stripeCheckoutRedirectUrl; + } else { + toast.error(response.errorMessage ?? "Failed to start checkout"); + } + } catch { + toast.error("Something went wrong. Please try again."); + } finally { + setPurchasingId(null); + } + }; + + return ( + +
+
+

+ Buy Credits +

+

+ One-time credit packs. No subscription required. +

+
+ +
+ {CREDIT_PACKS.map((pack) => ( + +
+ + ))} +
+ +
+ ); +} diff --git a/frontend/apps/artcraft-website/src/components/pricing-table/pricing-table.tsx b/frontend/apps/artcraft-website/src/components/pricing-table/pricing-table.tsx index 12df721907..23202cb0d9 100644 --- a/frontend/apps/artcraft-website/src/components/pricing-table/pricing-table.tsx +++ b/frontend/apps/artcraft-website/src/components/pricing-table/pricing-table.tsx @@ -497,7 +497,7 @@ const PricingTable = ({ {/* Manage Plan Button - Only shown if user has active subscription */} {activePlanSlug && activePlanSlug !== "free" && ( -
+
+
+ )} + {/* Footnotes */}

@@ -111,6 +155,13 @@ const Pricing = () => {

+ {isLoggedIn && ( + setCreditsModalOpen(false)} + /> + )} +
); diff --git a/frontend/libs/api/src/lib/BillingApi.ts b/frontend/libs/api/src/lib/BillingApi.ts index 58c5c40057..1b498e973f 100644 --- a/frontend/libs/api/src/lib/BillingApi.ts +++ b/frontend/libs/api/src/lib/BillingApi.ts @@ -151,6 +151,41 @@ export class BillingApi extends ApiManager { }); } + public async CreditsPackCheckout({ + creditsPack, + }: { + creditsPack: string; + }): Promise> { + const endpoint = `${this.getApiSchemeAndHost()}/v1/stripe_artcraft/checkout/credits_pack`; + + return await this.post< + { credits_pack: string }, + { + success: boolean; + stripe_checkout_redirect_url?: string; + error_message?: string; + } + >({ + endpoint: endpoint, + body: { + credits_pack: creditsPack, + }, + }) + .then((response) => ({ + success: response.success, + data: response.stripe_checkout_redirect_url + ? { stripeCheckoutRedirectUrl: response.stripe_checkout_redirect_url } + : undefined, + errorMessage: response.error_message, + })) + .catch((err) => { + return { + success: false, + errorMessage: err.message, + }; + }); + } + public async GetPortalUrl(): Promise< ApiResponse<{ stripePortalUrl: string }> > {