feat: implement Stripe checkout integration and add related API endpoints
This commit is contained in:
20
.env.example
20
.env.example
@@ -1,11 +1,17 @@
|
|||||||
# Firebase Configuration
|
# Firebase Configuration
|
||||||
# Get these values from your Firebase Console -> Project Settings -> General -> Your apps
|
# Get these values from your Firebase Console -> Project Settings -> General -> Your apps
|
||||||
FIREBASE_API_KEY=your_firebase_api_key_here
|
NUXT_PUBLIC_FIREBASE_API_KEY=your_firebase_api_key_here
|
||||||
FIREBASE_AUTH_DOMAIN=auctions-fb598.firebaseapp.com
|
NUXT_PUBLIC_FIREBASE_AUTH_DOMAIN=your_firebase_auth_domain_here
|
||||||
FIREBASE_PROJECT_ID=auctions-fb598
|
NUXT_PUBLIC_FIREBASE_PROJECT_ID=your_firebase_project_id_here
|
||||||
FIREBASE_STORAGE_BUCKET=auctions-fb598.appspot.com
|
NUXT_PUBLIC_FIREBASE_STORAGE_BUCKET=your_firebase_storage_bucket_here
|
||||||
FIREBASE_MESSAGING_SENDER_ID=your_messaging_sender_id_here
|
NUXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=your_messaging_sender_id_here
|
||||||
FIREBASE_APP_ID=your_app_id_here
|
NUXT_PUBLIC_FIREBASE_APP_ID=your_firebase_app_id_here
|
||||||
|
NUXT_PUBLIC_FIREBASE_MEASUREMENT_ID=your_firebase_measurement_id_here
|
||||||
|
|
||||||
|
# Stripe Configuration
|
||||||
|
NUXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_xxx
|
||||||
|
STRIPE_SECRET_KEY=sk_test_xxx
|
||||||
|
STRIPE_WEBHOOK_SECRET=whsec_xxx
|
||||||
|
|
||||||
# Backend Configuration
|
# Backend Configuration
|
||||||
BACKEND_URL=http://localhost:3000
|
NUXT_PUBLIC_BACKEND_URL=http://localhost:3000
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const { user, signOut, initAuth, isLoading } = useAuth()
|
const { user, signOut, initAuth, isLoading } = useAuth()
|
||||||
const showLoginModal = ref(false)
|
const loginModal = useLoginModal()
|
||||||
|
|
||||||
|
const openLoginModal = () => {
|
||||||
|
loginModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize auth on component mount
|
// Initialize auth on component mount
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -38,7 +42,7 @@ const handleSignOut = async () => {
|
|||||||
<div v-else-if="!isLoading" class="flex items-center gap-3">
|
<div v-else-if="!isLoading" class="flex items-center gap-3">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@click="showLoginModal = true"
|
@click="openLoginModal"
|
||||||
class="rounded-full border border-slate-700/80 px-4 py-2 text-sm font-medium text-slate-200 transition hover:border-slate-500 hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-sky-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-950"
|
class="rounded-full border border-slate-700/80 px-4 py-2 text-sm font-medium text-slate-200 transition hover:border-slate-500 hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-sky-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-950"
|
||||||
>
|
>
|
||||||
Login
|
Login
|
||||||
@@ -53,6 +57,6 @@ const handleSignOut = async () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<LoginModal v-model="showLoginModal" />
|
<LoginModal />
|
||||||
</nav>
|
</nav>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,40 +1,40 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const { signInWithEmail, signInWithGoogle, error } = useAuth()
|
const { signInWithEmail, signInWithGoogle, error } = useAuth();
|
||||||
|
|
||||||
const isOpen = defineModel<boolean>()
|
const isOpen = useLoginModal();
|
||||||
const email = ref('')
|
const email = ref("");
|
||||||
const password = ref('')
|
const password = ref("");
|
||||||
const loginError = ref<string | null>(null)
|
const loginError = ref<string | null>(null);
|
||||||
const isSubmitting = ref(false)
|
const isSubmitting = ref(false);
|
||||||
|
|
||||||
const handleEmailLogin = async () => {
|
const handleEmailLogin = async () => {
|
||||||
try {
|
try {
|
||||||
loginError.value = null
|
loginError.value = null;
|
||||||
isSubmitting.value = true
|
isSubmitting.value = true;
|
||||||
await signInWithEmail(email.value, password.value)
|
await signInWithEmail(email.value, password.value);
|
||||||
isOpen.value = false
|
isOpen.value = false;
|
||||||
// Reset form
|
// Reset form
|
||||||
email.value = ''
|
email.value = "";
|
||||||
password.value = ''
|
password.value = "";
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
loginError.value = 'Login failed. Please check your credentials.'
|
loginError.value = "Login failed. Please check your credentials.";
|
||||||
} finally {
|
} finally {
|
||||||
isSubmitting.value = false
|
isSubmitting.value = false;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleGoogleLogin = async () => {
|
const handleGoogleLogin = async () => {
|
||||||
try {
|
try {
|
||||||
loginError.value = null
|
loginError.value = null;
|
||||||
isSubmitting.value = true
|
isSubmitting.value = true;
|
||||||
await signInWithGoogle()
|
await signInWithGoogle();
|
||||||
isOpen.value = false
|
isOpen.value = false;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
loginError.value = 'Google login failed. Please try again.'
|
loginError.value = "Google login failed. Please try again.";
|
||||||
} finally {
|
} finally {
|
||||||
isSubmitting.value = false
|
isSubmitting.value = false;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -49,7 +49,9 @@ const handleGoogleLogin = async () => {
|
|||||||
|
|
||||||
<form @submit.prevent="handleEmailLogin" class="space-y-4">
|
<form @submit.prevent="handleEmailLogin" class="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-slate-300">Email</label>
|
<label class="block text-sm font-medium text-slate-300"
|
||||||
|
>Email</label
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
v-model="email"
|
v-model="email"
|
||||||
type="email"
|
type="email"
|
||||||
@@ -59,7 +61,9 @@ const handleGoogleLogin = async () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-slate-300">Password</label>
|
<label class="block text-sm font-medium text-slate-300"
|
||||||
|
>Password</label
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
v-model="password"
|
v-model="password"
|
||||||
type="password"
|
type="password"
|
||||||
@@ -73,7 +77,7 @@ const handleGoogleLogin = async () => {
|
|||||||
:disabled="isSubmitting"
|
:disabled="isSubmitting"
|
||||||
class="w-full rounded-md bg-sky-600 px-4 py-2 text-white hover:bg-sky-700 disabled:opacity-50"
|
class="w-full rounded-md bg-sky-600 px-4 py-2 text-white hover:bg-sky-700 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{{ isSubmitting ? 'Signing in...' : 'Sign In' }}
|
{{ isSubmitting ? "Signing in..." : "Sign In" }}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,16 @@ const props = defineProps<{
|
|||||||
templateLabel: string;
|
templateLabel: string;
|
||||||
productionPixels: number;
|
productionPixels: number;
|
||||||
isExporting: boolean;
|
isExporting: boolean;
|
||||||
|
isCheckoutPending: boolean;
|
||||||
|
checkoutPrice: number;
|
||||||
|
checkoutError: string | null;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: "download-preview"): void;
|
(e: "download-preview"): void;
|
||||||
(e: "download-production"): void;
|
(e: "download-production"): void;
|
||||||
(e: "export"): void;
|
(e: "export"): void;
|
||||||
|
(e: "checkout"): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const viewMode = ref<"flat" | "turntable">("flat");
|
const viewMode = ref<"flat" | "turntable">("flat");
|
||||||
@@ -23,6 +27,9 @@ const handleSelectView = (mode: "flat" | "turntable") => {
|
|||||||
const handleExport = () => emit("export");
|
const handleExport = () => emit("export");
|
||||||
const handleDownloadPreview = () => emit("download-preview");
|
const handleDownloadPreview = () => emit("download-preview");
|
||||||
const handleDownloadProduction = () => emit("download-production");
|
const handleDownloadProduction = () => emit("download-production");
|
||||||
|
const handleCheckout = () => emit("checkout");
|
||||||
|
|
||||||
|
const priceLabel = computed(() => props.checkoutPrice.toFixed(2));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -112,6 +119,20 @@ const handleDownloadProduction = () => emit("download-production");
|
|||||||
>
|
>
|
||||||
Download Print-Ready PNG
|
Download Print-Ready PNG
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex-1 rounded-xl bg-emerald-500 px-4 py-3 text-sm font-semibold text-emerald-950 transition hover:bg-emerald-400 disabled:cursor-not-allowed disabled:bg-emerald-500/60 disabled:text-emerald-900/60"
|
||||||
|
:disabled="props.isCheckoutPending || props.isExporting"
|
||||||
|
@click="handleCheckout"
|
||||||
|
>
|
||||||
|
{{ props.isCheckoutPending ? "Redirecting…" : `Buy This Design ($${priceLabel})` }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="props.checkoutError"
|
||||||
|
class="mt-3 rounded-xl border border-rose-500/60 bg-rose-500/10 px-4 py-3 text-sm text-rose-200"
|
||||||
|
>
|
||||||
|
{{ props.checkoutError }}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -76,8 +76,11 @@ export const useAuth = () => {
|
|||||||
const userCredential = await signInWithEmailAndPassword(auth, email, password)
|
const userCredential = await signInWithEmailAndPassword(auth, email, password)
|
||||||
const idToken = await userCredential.user.getIdToken()
|
const idToken = await userCredential.user.getIdToken()
|
||||||
|
|
||||||
// Send token to backend
|
try {
|
||||||
await authenticateWithBackend(idToken)
|
await authenticateWithBackend(idToken)
|
||||||
|
} catch (backendErr) {
|
||||||
|
console.warn('[useAuth] Backend authentication failed after email login:', backendErr)
|
||||||
|
}
|
||||||
|
|
||||||
return userCredential.user
|
return userCredential.user
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@@ -103,8 +106,11 @@ export const useAuth = () => {
|
|||||||
const userCredential = await signInWithPopup(auth, provider)
|
const userCredential = await signInWithPopup(auth, provider)
|
||||||
const idToken = await userCredential.user.getIdToken()
|
const idToken = await userCredential.user.getIdToken()
|
||||||
|
|
||||||
// Send token to backend
|
try {
|
||||||
await authenticateWithBackend(idToken)
|
await authenticateWithBackend(idToken)
|
||||||
|
} catch (backendErr) {
|
||||||
|
console.warn('[useAuth] Backend authentication failed after Google login:', backendErr)
|
||||||
|
}
|
||||||
|
|
||||||
return userCredential.user
|
return userCredential.user
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
|||||||
1
app/composables/useLoginModal.ts
Normal file
1
app/composables/useLoginModal.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const useLoginModal = () => useState<boolean>('login-modal-open', () => false);
|
||||||
@@ -5,6 +5,7 @@ import DesignerToolbar from "~/components/designer/DesignerToolbar.vue";
|
|||||||
import TemplatePicker from "~/components/designer/TemplatePicker.vue";
|
import TemplatePicker from "~/components/designer/TemplatePicker.vue";
|
||||||
|
|
||||||
import { useSlipmatDesigner } from "~/composables/useSlipmatDesigner";
|
import { useSlipmatDesigner } from "~/composables/useSlipmatDesigner";
|
||||||
|
import type { ExportedDesign } from "~/composables/useSlipmatDesigner";
|
||||||
|
|
||||||
const {
|
const {
|
||||||
templates,
|
templates,
|
||||||
@@ -38,12 +39,88 @@ const {
|
|||||||
resetZoom,
|
resetZoom,
|
||||||
} = useSlipmatDesigner();
|
} = useSlipmatDesigner();
|
||||||
|
|
||||||
|
const DESIGN_PRICE_USD = 39.99;
|
||||||
|
|
||||||
|
const { user } = useAuth();
|
||||||
|
const loginModal = useLoginModal();
|
||||||
|
|
||||||
|
const isCheckoutPending = ref(false);
|
||||||
|
const checkoutError = ref<string | null>(null);
|
||||||
|
const lastExportedDesign = ref<ExportedDesign | null>(null);
|
||||||
|
|
||||||
const handleTemplateSelect = (templateId: string) => {
|
const handleTemplateSelect = (templateId: string) => {
|
||||||
selectTemplate(templateId);
|
selectTemplate(templateId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleExport = async () => {
|
const handleExport = async () => {
|
||||||
await exportDesign();
|
const result = await exportDesign();
|
||||||
|
if (result) {
|
||||||
|
lastExportedDesign.value = result;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCheckout = async () => {
|
||||||
|
if (!process.client) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.value) {
|
||||||
|
loginModal.value = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
checkoutError.value = null;
|
||||||
|
isCheckoutPending.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
let exportResult = await exportDesign();
|
||||||
|
if (!exportResult) {
|
||||||
|
exportResult = lastExportedDesign.value;
|
||||||
|
}
|
||||||
|
if (!exportResult) {
|
||||||
|
throw new Error("Unable to export the current design. Please try again.");
|
||||||
|
}
|
||||||
|
lastExportedDesign.value = exportResult;
|
||||||
|
|
||||||
|
const designId =
|
||||||
|
typeof crypto !== "undefined" && "randomUUID" in crypto
|
||||||
|
? crypto.randomUUID()
|
||||||
|
: `design-${Date.now()}`;
|
||||||
|
|
||||||
|
const successUrlTemplate = `${window.location.origin}/checkout/success?session_id={CHECKOUT_SESSION_ID}`;
|
||||||
|
const cancelUrl = window.location.href;
|
||||||
|
|
||||||
|
const session = await $fetch<{ id: string; url?: string | null }>("/api/checkout.session", {
|
||||||
|
method: "POST",
|
||||||
|
body: {
|
||||||
|
designId,
|
||||||
|
templateId: exportResult.templateId,
|
||||||
|
designName: templateLabel.value,
|
||||||
|
amount: DESIGN_PRICE_USD,
|
||||||
|
currency: "usd",
|
||||||
|
successUrl: successUrlTemplate,
|
||||||
|
cancelUrl,
|
||||||
|
customerEmail: user.value?.email,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!session?.id) {
|
||||||
|
throw new Error("Stripe session could not be created.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (session.url) {
|
||||||
|
window.location.href = session.url;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fallbackRedirect = successUrlTemplate.replace("{CHECKOUT_SESSION_ID}", session.id);
|
||||||
|
window.location.href = fallbackRedirect;
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error("Checkout failed", err);
|
||||||
|
checkoutError.value = err?.message ?? "Unable to start checkout. Please try again.";
|
||||||
|
} finally {
|
||||||
|
isCheckoutPending.value = false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -58,11 +135,6 @@ const handleExport = async () => {
|
|||||||
<h1 class="text-3xl font-bold text-white sm:text-4xl">
|
<h1 class="text-3xl font-bold text-white sm:text-4xl">
|
||||||
Craft custom slipmats ready for the pressing plant.
|
Craft custom slipmats ready for the pressing plant.
|
||||||
</h1>
|
</h1>
|
||||||
<p class="max-w-3xl text-base text-slate-300">
|
|
||||||
Pick a template, drop in artwork, and we’ll generate both a high-fidelity
|
|
||||||
preview and a print-ready PNG at exact specs. Everything stays within a
|
|
||||||
circular safe zone to ensure clean results on vinyl.
|
|
||||||
</p>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section class="mt-10 grid gap-8 lg:grid-cols-[320px_minmax(0,1fr)]">
|
<section class="mt-10 grid gap-8 lg:grid-cols-[320px_minmax(0,1fr)]">
|
||||||
@@ -98,9 +170,13 @@ const handleExport = async () => {
|
|||||||
:template-label="templateLabel"
|
:template-label="templateLabel"
|
||||||
:production-pixels="productionPixelSize"
|
:production-pixels="productionPixelSize"
|
||||||
:is-exporting="isExporting"
|
:is-exporting="isExporting"
|
||||||
|
:is-checkout-pending="isCheckoutPending"
|
||||||
|
:checkout-price="DESIGN_PRICE_USD"
|
||||||
|
:checkout-error="checkoutError"
|
||||||
@export="handleExport"
|
@export="handleExport"
|
||||||
@download-preview="downloadPreview"
|
@download-preview="downloadPreview"
|
||||||
@download-production="downloadProduction"
|
@download-production="downloadProduction"
|
||||||
|
@checkout="handleCheckout"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ export default defineNuxtConfig({
|
|||||||
plugins: [tailwindcss()],
|
plugins: [tailwindcss()],
|
||||||
},
|
},
|
||||||
runtimeConfig: {
|
runtimeConfig: {
|
||||||
|
stripeSecretKey: process.env.STRIPE_SECRET_KEY,
|
||||||
|
stripeWebhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
|
||||||
public: {
|
public: {
|
||||||
firebaseApiKey: process.env.NUXT_PUBLIC_FIREBASE_API_KEY,
|
firebaseApiKey: process.env.NUXT_PUBLIC_FIREBASE_API_KEY,
|
||||||
firebaseAuthDomain: process.env.NUXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
|
firebaseAuthDomain: process.env.NUXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
|
||||||
@@ -18,6 +20,7 @@ export default defineNuxtConfig({
|
|||||||
firebaseMessagingSenderId: process.env.NUXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
|
firebaseMessagingSenderId: process.env.NUXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
|
||||||
firebaseAppId: process.env.NUXT_PUBLIC_FIREBASE_APP_ID,
|
firebaseAppId: process.env.NUXT_PUBLIC_FIREBASE_APP_ID,
|
||||||
firebaseMeasurementId: process.env.NUXT_PUBLIC_FIREBASE_MEASUREMENT_ID,
|
firebaseMeasurementId: process.env.NUXT_PUBLIC_FIREBASE_MEASUREMENT_ID,
|
||||||
|
stripePublishableKey: process.env.NUXT_PUBLIC_STRIPE_PUBLISHABLE_KEY,
|
||||||
backendUrl: process.env.NUXT_PUBLIC_BACKEND_URL || 'http://localhost:3000'
|
backendUrl: process.env.NUXT_PUBLIC_BACKEND_URL || 'http://localhost:3000'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
146
package-lock.json
generated
146
package-lock.json
generated
@@ -12,6 +12,7 @@
|
|||||||
"fabric": "^6.0.2",
|
"fabric": "^6.0.2",
|
||||||
"firebase": "^12.5.0",
|
"firebase": "^12.5.0",
|
||||||
"nuxt": "^4.2.0",
|
"nuxt": "^4.2.0",
|
||||||
|
"stripe": "^19.3.0",
|
||||||
"tailwindcss": "^4.1.16",
|
"tailwindcss": "^4.1.16",
|
||||||
"vue": "^3.5.22",
|
"vue": "^3.5.22",
|
||||||
"vue-router": "^4.6.3"
|
"vue-router": "^4.6.3"
|
||||||
@@ -5117,7 +5118,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-errors": "^1.3.0",
|
"es-errors": "^1.3.0",
|
||||||
"function-bind": "^1.1.2"
|
"function-bind": "^1.1.2"
|
||||||
@@ -5126,6 +5126,22 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/call-bound": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
|
"get-intrinsic": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/caniuse-api": {
|
"node_modules/caniuse-api": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz",
|
||||||
@@ -6383,7 +6399,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bind-apply-helpers": "^1.0.1",
|
"call-bind-apply-helpers": "^1.0.1",
|
||||||
"es-errors": "^1.3.0",
|
"es-errors": "^1.3.0",
|
||||||
@@ -6477,7 +6492,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
@@ -6487,7 +6501,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
@@ -6503,7 +6516,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-errors": "^1.3.0"
|
"es-errors": "^1.3.0"
|
||||||
},
|
},
|
||||||
@@ -7130,7 +7142,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bind-apply-helpers": "^1.0.2",
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
"es-define-property": "^1.0.1",
|
"es-define-property": "^1.0.1",
|
||||||
@@ -7161,7 +7172,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dunder-proto": "^1.0.1",
|
"dunder-proto": "^1.0.1",
|
||||||
"es-object-atoms": "^1.0.0"
|
"es-object-atoms": "^1.0.0"
|
||||||
@@ -7290,7 +7300,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
},
|
},
|
||||||
@@ -7347,7 +7356,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
},
|
},
|
||||||
@@ -8605,7 +8613,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
@@ -9249,6 +9256,18 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/object-inspect": {
|
||||||
|
"version": "1.13.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||||
|
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ofetch": {
|
"node_modules/ofetch": {
|
||||||
"version": "1.5.0",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.5.0.tgz",
|
||||||
@@ -10232,6 +10251,21 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/qs": {
|
||||||
|
"version": "6.14.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
|
||||||
|
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"side-channel": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/quansync": {
|
"node_modules/quansync": {
|
||||||
"version": "0.2.11",
|
"version": "0.2.11",
|
||||||
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",
|
||||||
@@ -10793,6 +10827,78 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/side-channel": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"object-inspect": "^1.13.3",
|
||||||
|
"side-channel-list": "^1.0.0",
|
||||||
|
"side-channel-map": "^1.0.1",
|
||||||
|
"side-channel-weakmap": "^1.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/side-channel-list": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"object-inspect": "^1.13.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/side-channel-map": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bound": "^1.0.2",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"get-intrinsic": "^1.2.5",
|
||||||
|
"object-inspect": "^1.13.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/side-channel-weakmap": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bound": "^1.0.2",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"get-intrinsic": "^1.2.5",
|
||||||
|
"object-inspect": "^1.13.3",
|
||||||
|
"side-channel-map": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/signal-exit": {
|
"node_modules/signal-exit": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||||
@@ -11116,6 +11222,26 @@
|
|||||||
"integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
|
"integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/stripe": {
|
||||||
|
"version": "19.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/stripe/-/stripe-19.3.0.tgz",
|
||||||
|
"integrity": "sha512-3MbqRkw5LXb4LWP1LgIEYxUAYhYDDU5pcHZj4Xha6VWPnN1wrUmQ7Htsgm8wR584s0hn1aQg1lYD0Hi+F37E5g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"qs": "^6.11.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/node": ">=16"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/node": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/structured-clone-es": {
|
"node_modules/structured-clone-es": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/structured-clone-es/-/structured-clone-es-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/structured-clone-es/-/structured-clone-es-1.0.0.tgz",
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"fabric": "^6.0.2",
|
"fabric": "^6.0.2",
|
||||||
"firebase": "^12.5.0",
|
"firebase": "^12.5.0",
|
||||||
"nuxt": "^4.2.0",
|
"nuxt": "^4.2.0",
|
||||||
|
"stripe": "^19.3.0",
|
||||||
"tailwindcss": "^4.1.16",
|
"tailwindcss": "^4.1.16",
|
||||||
"vue": "^3.5.22",
|
"vue": "^3.5.22",
|
||||||
"vue-router": "^4.6.3"
|
"vue-router": "^4.6.3"
|
||||||
|
|||||||
62
server/api/checkout.session.post.ts
Normal file
62
server/api/checkout.session.post.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import Stripe from 'stripe'
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const stripeSecretKey = config.stripeSecretKey
|
||||||
|
|
||||||
|
if (!stripeSecretKey) {
|
||||||
|
throw createError({ statusCode: 500, statusMessage: 'Stripe secret key not configured' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const stripe = new Stripe(stripeSecretKey, {
|
||||||
|
apiVersion: '2024-10-28.acacia',
|
||||||
|
})
|
||||||
|
|
||||||
|
const body = await readBody<{
|
||||||
|
designId: string
|
||||||
|
templateId?: string
|
||||||
|
designName?: string
|
||||||
|
amount: number
|
||||||
|
currency?: string
|
||||||
|
successUrl: string
|
||||||
|
cancelUrl: string
|
||||||
|
customerEmail?: string
|
||||||
|
}>(event)
|
||||||
|
|
||||||
|
if (!body?.designId || !body?.amount || !body?.successUrl || !body?.cancelUrl) {
|
||||||
|
throw createError({ statusCode: 400, statusMessage: 'Missing required fields' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const { currency = 'usd' } = body
|
||||||
|
|
||||||
|
const session = await stripe.checkout.sessions.create({
|
||||||
|
mode: 'payment',
|
||||||
|
payment_method_types: ['card'],
|
||||||
|
billing_address_collection: 'auto',
|
||||||
|
customer_email: body.customerEmail,
|
||||||
|
line_items: [
|
||||||
|
{
|
||||||
|
quantity: 1,
|
||||||
|
price_data: {
|
||||||
|
currency,
|
||||||
|
unit_amount: Math.round(body.amount * 100),
|
||||||
|
product_data: {
|
||||||
|
name: body.designName ?? `Slipmat design ${body.designId}`,
|
||||||
|
metadata: {
|
||||||
|
designId: body.designId,
|
||||||
|
...(body.templateId ? { templateId: body.templateId } : {}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
metadata: {
|
||||||
|
designId: body.designId,
|
||||||
|
...(body.templateId ? { templateId: body.templateId } : {}),
|
||||||
|
},
|
||||||
|
success_url: body.successUrl,
|
||||||
|
cancel_url: body.cancelUrl,
|
||||||
|
})
|
||||||
|
|
||||||
|
return { id: session.id, url: session.url }
|
||||||
|
})
|
||||||
36
server/api/transactions.post.ts
Normal file
36
server/api/transactions.post.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const body = await readBody<{
|
||||||
|
stripeSessionId: string
|
||||||
|
designId: string
|
||||||
|
templateId?: string
|
||||||
|
amount: number
|
||||||
|
currency: string
|
||||||
|
customerEmail?: string
|
||||||
|
assets?: {
|
||||||
|
previewUrl?: string
|
||||||
|
productionUrl?: string
|
||||||
|
}
|
||||||
|
}>(event)
|
||||||
|
|
||||||
|
if (!body?.stripeSessionId || !body?.designId) {
|
||||||
|
throw createError({ statusCode: 400, statusMessage: 'Missing required fields' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Persist the transaction to your database of choice.
|
||||||
|
// Example shape:
|
||||||
|
// await db.transaction.create({
|
||||||
|
// stripeSessionId: body.stripeSessionId,
|
||||||
|
// designId: body.designId,
|
||||||
|
// templateId: body.templateId,
|
||||||
|
// amount: body.amount,
|
||||||
|
// currency: body.currency,
|
||||||
|
// customerEmail: body.customerEmail,
|
||||||
|
// previewUrl: body.assets?.previewUrl,
|
||||||
|
// productionUrl: body.assets?.productionUrl,
|
||||||
|
// })
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
receivedAt: new Date().toISOString(),
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user