feat: refactor payment processing and checkout flow with Stripe integration
- Removed AcquistoProcessing component and replaced it with a new CheckoutForm component for better separation of concerns. - Added new payment checkout page to handle payment intents and display payment status. - Updated server-side logic to create payment intents and retrieve payment data for checkout. - Enhanced error handling and user feedback during payment processing. - Introduced new utility functions for fetching payment data and managing Stripe integration.
This commit is contained in:
parent
627267b86f
commit
52801bebd1
13 changed files with 625 additions and 464 deletions
|
|
@ -1,243 +0,0 @@
|
||||||
"use client";
|
|
||||||
import {
|
|
||||||
Elements,
|
|
||||||
PaymentElement,
|
|
||||||
useElements,
|
|
||||||
useStripe,
|
|
||||||
} from "@stripe/react-stripe-js";
|
|
||||||
import type { Stripe } from "@stripe/stripe-js";
|
|
||||||
import { ArrowRight, ExternalLink } from "lucide-react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { type FormEvent, useState } from "react";
|
|
||||||
import LoadingButton from "~/components/custom_ui/loading-button";
|
|
||||||
import { LoadingPage } from "~/components/loading";
|
|
||||||
import { Status500 } from "~/components/status-page";
|
|
||||||
import { Button } from "~/components/ui/button";
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
|
||||||
import { env } from "~/env";
|
|
||||||
import { formatCurrency } from "~/lib/utils";
|
|
||||||
import { useTranslation } from "~/providers/I18nProvider";
|
|
||||||
import type { PaymentsId } from "~/schemas/public/Payments";
|
|
||||||
import type { PrezziarioIdprezziario } from "~/schemas/public/Prezziario";
|
|
||||||
import { api } from "~/utils/api";
|
|
||||||
|
|
||||||
type AcquistoProcessingProps = {
|
|
||||||
paymentId: PaymentsId;
|
|
||||||
packId: PrezziarioIdprezziario;
|
|
||||||
stripePromise: Promise<Stripe | null>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AcquistoProcessing = ({
|
|
||||||
paymentId,
|
|
||||||
packId,
|
|
||||||
stripePromise,
|
|
||||||
}: AcquistoProcessingProps) => {
|
|
||||||
const [stripeIntent, setStripeIntent] = useState<{
|
|
||||||
clientSecret: string | null;
|
|
||||||
titolo: string;
|
|
||||||
prezzo: number;
|
|
||||||
descrizione: string;
|
|
||||||
} | null>(null);
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [generatedIntent, setGeneratedIntent] = useState(false);
|
|
||||||
|
|
||||||
const { mutate, isPending } = api.stripe.createIntent.useMutation({
|
|
||||||
onSuccess: (data) => {
|
|
||||||
setStripeIntent(data);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (generatedIntent && !isPending && !stripeIntent) {
|
|
||||||
return <Status500 />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mx-auto my-4 w-full max-w-3xl space-y-8 rounded-md">
|
|
||||||
<h3 className="mt-2 text-center font-bold text-2xl uppercase tracking-wide">
|
|
||||||
{t.acquisto.titolo}
|
|
||||||
</h3>
|
|
||||||
{(() => {
|
|
||||||
if (!generatedIntent) {
|
|
||||||
return (
|
|
||||||
<PricePreparation
|
|
||||||
onProcedi={() => {
|
|
||||||
setGeneratedIntent(true);
|
|
||||||
mutate({
|
|
||||||
paymentId,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
packId={packId}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (isPending) {
|
|
||||||
return <LoadingPage />;
|
|
||||||
}
|
|
||||||
if (stripeIntent?.clientSecret) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Elements
|
|
||||||
options={{
|
|
||||||
appearance: {
|
|
||||||
theme: "stripe",
|
|
||||||
variables: {
|
|
||||||
colorPrimary: "#18181b",
|
|
||||||
colorText: "#18181b",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
clientSecret: stripeIntent.clientSecret,
|
|
||||||
}}
|
|
||||||
stripe={stripePromise}
|
|
||||||
>
|
|
||||||
<CheckoutForm
|
|
||||||
descrizione={stripeIntent.descrizione}
|
|
||||||
prezzo={stripeIntent.prezzo}
|
|
||||||
titolo={stripeIntent.titolo}
|
|
||||||
/>
|
|
||||||
</Elements>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})()}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const PricePreparation = ({
|
|
||||||
packId,
|
|
||||||
onProcedi,
|
|
||||||
}: {
|
|
||||||
packId: PrezziarioIdprezziario;
|
|
||||||
onProcedi: () => void;
|
|
||||||
}) => {
|
|
||||||
const { data: prezziario, isLoading } =
|
|
||||||
api.prezziario.getPrezzoPerServizio.useQuery({
|
|
||||||
idprezziario: packId,
|
|
||||||
});
|
|
||||||
const { t } = useTranslation();
|
|
||||||
return (
|
|
||||||
<div className="mx-auto mt-4 max-w-2xl p-2 sm:mt-10">
|
|
||||||
{isLoading || !prezziario ? (
|
|
||||||
<>
|
|
||||||
<h3 className="font-semibold text-2xl">{t.acquisto.preparazione}</h3>
|
|
||||||
<LoadingPage />
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-8">
|
|
||||||
<div className="space-y-6">
|
|
||||||
<p className="text-3xl">{prezziario.nome_it}</p>
|
|
||||||
<p className="text-lg">
|
|
||||||
Cod: “{prezziario.idprezziario}”
|
|
||||||
</p>
|
|
||||||
<p className="font-bold text-4xl">
|
|
||||||
{formatCurrency(prezziario.prezzo_cent / 100)}
|
|
||||||
</p>
|
|
||||||
<p>{prezziario.desc_it}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{prezziario.testo_condizioni && (
|
|
||||||
<Link
|
|
||||||
aria-label="Leggi le condizioni"
|
|
||||||
className="block w-fit"
|
|
||||||
href={`/servizio/condizioni/${prezziario.testo_condizioni}`}
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<Button className="w-full text-sm">
|
|
||||||
<span>{t.acquisto.leggi_condizioni}</span>
|
|
||||||
<ExternalLink />
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button
|
|
||||||
className="w-full text-lg"
|
|
||||||
disabled={isLoading || !prezziario}
|
|
||||||
onClick={onProcedi}
|
|
||||||
size="xl"
|
|
||||||
variant="info"
|
|
||||||
>
|
|
||||||
<span>{t.acquisto.procedi_pagamento}</span> <ArrowRight />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const CheckoutForm = ({
|
|
||||||
titolo,
|
|
||||||
prezzo,
|
|
||||||
descrizione,
|
|
||||||
}: {
|
|
||||||
titolo: string;
|
|
||||||
prezzo: number;
|
|
||||||
descrizione: string;
|
|
||||||
}) => {
|
|
||||||
const stripe = useStripe();
|
|
||||||
const elements = useElements();
|
|
||||||
|
|
||||||
const [message, setMessage] = useState<string | null>(null);
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
|
||||||
e.preventDefault();
|
|
||||||
console.log("submitting payment");
|
|
||||||
if (!stripe || !elements) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsLoading(true);
|
|
||||||
const { error } = await stripe.confirmPayment({
|
|
||||||
confirmParams: {
|
|
||||||
return_url: `${env.NEXT_PUBLIC_BASE_URL}/servizio/acquisto-processing`,
|
|
||||||
},
|
|
||||||
elements,
|
|
||||||
});
|
|
||||||
if (error.type === "card_error" || error.type === "validation_error") {
|
|
||||||
setMessage(error.message || "An unexpected error occurred.");
|
|
||||||
} else {
|
|
||||||
setMessage("An unexpected error occurred.");
|
|
||||||
}
|
|
||||||
setIsLoading(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className="flex w-full flex-col justify-between text-left">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>
|
|
||||||
<p className="text-2xl">{titolo}</p>
|
|
||||||
</CardTitle>
|
|
||||||
<p>{descrizione}</p>
|
|
||||||
<span>
|
|
||||||
<span className="font-bold text-3xl">
|
|
||||||
{formatCurrency(prezzo / 100)}
|
|
||||||
</span>{" "}
|
|
||||||
<span className="font-normal text-base sm:text-sm">
|
|
||||||
{t.pricing_cmp.risultati_iva}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<form className="space-y-5" id="payment-form" onSubmit={handleSubmit}>
|
|
||||||
<PaymentElement
|
|
||||||
id="payment-element"
|
|
||||||
options={{ layout: "accordion" }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<LoadingButton
|
|
||||||
aria-label="Procedi al pagamento"
|
|
||||||
className="w-full"
|
|
||||||
disabled={isLoading || !stripe || !elements}
|
|
||||||
loading={isLoading || !stripe || !elements}
|
|
||||||
type="submit"
|
|
||||||
>
|
|
||||||
{t.procedi}
|
|
||||||
</LoadingButton>
|
|
||||||
{message && <div id="payment-message">{message}</div>}
|
|
||||||
</form>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
187
apps/infoalloggi/src/components/checkout.tsx
Normal file
187
apps/infoalloggi/src/components/checkout.tsx
Normal file
|
|
@ -0,0 +1,187 @@
|
||||||
|
import {
|
||||||
|
Elements,
|
||||||
|
PaymentElement,
|
||||||
|
useElements,
|
||||||
|
useStripe,
|
||||||
|
} from "@stripe/react-stripe-js";
|
||||||
|
import { loadStripe } from "@stripe/stripe-js";
|
||||||
|
import { type FormEvent, useState } from "react";
|
||||||
|
import { env } from "~/env";
|
||||||
|
import { formatCurrency } from "~/lib/utils";
|
||||||
|
import { useTranslation } from "~/providers/I18nProvider";
|
||||||
|
import LoadingButton from "./custom_ui/loading-button";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card";
|
||||||
|
|
||||||
|
const stripePromise = loadStripe(env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY);
|
||||||
|
|
||||||
|
export default function CheckoutForm({
|
||||||
|
clientSecret,
|
||||||
|
titolo,
|
||||||
|
prezzo,
|
||||||
|
descrizione,
|
||||||
|
}: {
|
||||||
|
clientSecret: string;
|
||||||
|
titolo: string;
|
||||||
|
prezzo: number;
|
||||||
|
descrizione: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Elements
|
||||||
|
options={{
|
||||||
|
appearance: {
|
||||||
|
theme: "stripe",
|
||||||
|
variables: {
|
||||||
|
colorPrimary: "#18181b",
|
||||||
|
colorText: "#18181b",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
clientSecret,
|
||||||
|
}}
|
||||||
|
stripe={stripePromise}
|
||||||
|
>
|
||||||
|
<PaymentForm descrizione={descrizione} prezzo={prezzo} titolo={titolo} />
|
||||||
|
</Elements>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const PaymentForm = ({
|
||||||
|
titolo,
|
||||||
|
prezzo,
|
||||||
|
descrizione,
|
||||||
|
}: {
|
||||||
|
titolo: string;
|
||||||
|
prezzo: number;
|
||||||
|
descrizione: string;
|
||||||
|
}) => {
|
||||||
|
const stripe = useStripe();
|
||||||
|
const elements = useElements();
|
||||||
|
|
||||||
|
const [message, setMessage] = useState<string | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
console.log("submitting payment");
|
||||||
|
if (!stripe || !elements) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
const { error } = await stripe.confirmPayment({
|
||||||
|
confirmParams: {
|
||||||
|
return_url: `${env.NEXT_PUBLIC_BASE_URL}/servizio/pagamento-status`,
|
||||||
|
},
|
||||||
|
elements,
|
||||||
|
});
|
||||||
|
if (error.type === "card_error" || error.type === "validation_error") {
|
||||||
|
setMessage(error.message || "An unexpected error occurred.");
|
||||||
|
} else {
|
||||||
|
setMessage("An unexpected error occurred.");
|
||||||
|
}
|
||||||
|
setIsLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="flex w-full flex-col justify-between text-left">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>
|
||||||
|
<p className="text-2xl">{titolo}</p>
|
||||||
|
</CardTitle>
|
||||||
|
<p>{descrizione}</p>
|
||||||
|
<span>
|
||||||
|
<span className="font-bold text-3xl">
|
||||||
|
{formatCurrency(prezzo / 100)}
|
||||||
|
</span>{" "}
|
||||||
|
<span className="font-normal text-base sm:text-sm">
|
||||||
|
{t.pricing_cmp.risultati_iva}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<form className="space-y-5" id="payment-form" onSubmit={handleSubmit}>
|
||||||
|
<PaymentElement
|
||||||
|
id="payment-element"
|
||||||
|
options={{ layout: "accordion" }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LoadingButton
|
||||||
|
aria-label="Procedi al pagamento"
|
||||||
|
className="w-full"
|
||||||
|
disabled={isLoading || !stripe || !elements}
|
||||||
|
loading={isLoading || !stripe || !elements}
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
{t.procedi}
|
||||||
|
</LoadingButton>
|
||||||
|
{message && <div id="payment-message">{message}</div>}
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
/*
|
||||||
|
function PaymentForm2() {
|
||||||
|
const stripe = useStripe();
|
||||||
|
const elements = useElements();
|
||||||
|
|
||||||
|
const [message, setMessage] = useState<string | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!stripe || !elements) {
|
||||||
|
// Stripe.js hasn't yet loaded.
|
||||||
|
// Make sure to disable form submission until Stripe.js has loaded.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
const { error } = await stripe.confirmPayment({
|
||||||
|
elements,
|
||||||
|
confirmParams: {
|
||||||
|
// Make sure to change this to your payment completion page
|
||||||
|
return_url: "http://localhost:3000/success",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// This point will only be reached if there is an immediate error when
|
||||||
|
// confirming the payment. Otherwise, your customer will be redirected to
|
||||||
|
// your `return_url`. For some payment methods like iDEAL, your customer will
|
||||||
|
// be redirected to an intermediate site first to authorize the payment, then
|
||||||
|
// redirected to the `return_url`.
|
||||||
|
if (error.type === "card_error" || error.type === "validation_error") {
|
||||||
|
setMessage(error.message || "Validation error occurred.");
|
||||||
|
} else {
|
||||||
|
setMessage("An unexpected error occurred.");
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form id="payment-form" onSubmit={handleSubmit}>
|
||||||
|
<PaymentElement
|
||||||
|
id="payment-element"
|
||||||
|
options={{
|
||||||
|
layout: "accordion",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
disabled={isLoading || !stripe || !elements}
|
||||||
|
id="submit"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
<span id="button-text">
|
||||||
|
{isLoading ? <div className="spinner" id="spinner"></div> : "Pay now"}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{message && <div id="payment-message">{message}</div>}
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
@ -153,6 +153,7 @@ export const PaymentsTable = ({
|
||||||
{
|
{
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const data = row.original;
|
const data = row.original;
|
||||||
|
if (!isAdmin) return null;
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
|
|
|
||||||
12
apps/infoalloggi/src/lib/stripe.ts
Normal file
12
apps/infoalloggi/src/lib/stripe.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
// lib/stripe.js
|
||||||
|
import Stripe from "stripe";
|
||||||
|
import { env } from "~/env";
|
||||||
|
|
||||||
|
// This check ensures we don't accidentally run this on the client
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
throw new Error("Stripe Secret Key should not be loaded on the client!");
|
||||||
|
}
|
||||||
|
|
||||||
|
const stripe = new Stripe(env.STRIPE_SECRET_KEY);
|
||||||
|
|
||||||
|
export default stripe;
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import Stripe from "stripe";
|
import type Stripe from "stripe";
|
||||||
import { env } from "~/env";
|
import { env } from "~/env";
|
||||||
|
import stripe from "~/lib/stripe";
|
||||||
import type { PaymentsId } from "~/schemas/public/Payments";
|
import type { PaymentsId } from "~/schemas/public/Payments";
|
||||||
import { appRouter } from "~/server/api/root";
|
import { appRouter } from "~/server/api/root";
|
||||||
import { createTRPCContext, t } from "~/server/api/trpc";
|
import { createTRPCContext, t } from "~/server/api/trpc";
|
||||||
|
|
@ -9,8 +10,6 @@ const handler = async (
|
||||||
req: NextApiRequest,
|
req: NextApiRequest,
|
||||||
res: NextApiResponse,
|
res: NextApiResponse,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const stripe = new Stripe(env.STRIPE_SECRET_KEY);
|
|
||||||
|
|
||||||
if (req.method === "POST") {
|
if (req.method === "POST") {
|
||||||
const ctx = await createTRPCContext({ req: req, res: res });
|
const ctx = await createTRPCContext({ req: req, res: res });
|
||||||
const createCaller = t.createCallerFactory(appRouter);
|
const createCaller = t.createCallerFactory(appRouter);
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,14 @@ const StripeSection = () => {
|
||||||
<h4 className="mb-2 font-semibold text-lg">
|
<h4 className="mb-2 font-semibold text-lg">
|
||||||
Stripe Status: {data?.success ? "Sano" : "Non sano"}
|
Stripe Status: {data?.success ? "Sano" : "Non sano"}
|
||||||
</h4>
|
</h4>
|
||||||
|
<p>Keys:</p>
|
||||||
|
<p>
|
||||||
|
Secret Key: <span className="wrap-anywhere">{data?.secretKey}</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Public Key: <span className="wrap-anywhere">{data?.publicKey}</span>
|
||||||
|
</p>
|
||||||
|
<hr />
|
||||||
<p>Endpoint Webhook:</p>
|
<p>Endpoint Webhook:</p>
|
||||||
{data?.endpoints?.data.map((endpoint) => (
|
{data?.endpoints?.data.map((endpoint) => (
|
||||||
<div className="mb-2" key={endpoint.id}>
|
<div className="mb-2" key={endpoint.id}>
|
||||||
|
|
|
||||||
|
|
@ -1,208 +0,0 @@
|
||||||
import { Elements, useStripe } from "@stripe/react-stripe-js";
|
|
||||||
import { loadStripe } from "@stripe/stripe-js";
|
|
||||||
import { ArrowLeft } from "lucide-react";
|
|
||||||
import type { GetServerSideProps } from "next";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import PaymentStatus from "~/components/payment_status";
|
|
||||||
import { ProgressRedirect } from "~/components/progress_redirect";
|
|
||||||
import { Button, buttonVariants } from "~/components/ui/button";
|
|
||||||
import { env } from "~/env";
|
|
||||||
import { cn } from "~/lib/utils";
|
|
||||||
import { useTranslation } from "~/providers/I18nProvider";
|
|
||||||
import PaymentStatusEnum from "~/schemas/public/PaymentStatusEnum";
|
|
||||||
import { api } from "~/utils/api";
|
|
||||||
|
|
||||||
type AcquistoProcessingProps = {
|
|
||||||
client_secret: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getServerSideProps = (async (context) => {
|
|
||||||
if (
|
|
||||||
!context.query.payment_intent_client_secret ||
|
|
||||||
typeof context.query.payment_intent_client_secret !== "string"
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
destination: "/500",
|
|
||||||
permanent: false,
|
|
||||||
statusCode: 500,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
client_secret: context.query.payment_intent_client_secret,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}) satisfies GetServerSideProps<AcquistoProcessingProps>;
|
|
||||||
|
|
||||||
export default function AcquistoProcessing({
|
|
||||||
client_secret,
|
|
||||||
}: AcquistoProcessingProps) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const stripePromise = loadStripe(env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mx-auto my-5 w-full max-w-lg space-y-8">
|
|
||||||
<h3 className="my-8 text-center font-bold text-neutral-700 text-xl uppercase tracking-wide dark:text-white">
|
|
||||||
{t.acquisto_elaborazione.titolo}
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Elements
|
|
||||||
options={{
|
|
||||||
appearance: {
|
|
||||||
theme: "stripe",
|
|
||||||
},
|
|
||||||
clientSecret: client_secret,
|
|
||||||
}}
|
|
||||||
stripe={stripePromise}
|
|
||||||
>
|
|
||||||
<CompletePage secret={client_secret} />
|
|
||||||
</Elements>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const CompletePage = ({ secret }: { secret: string }) => {
|
|
||||||
const stripe = useStripe();
|
|
||||||
const utils = api.useUtils();
|
|
||||||
const router = useRouter();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [status, setStatus] = useState<
|
|
||||||
"success" | "processing" | "failed" | "internal_error" | "not_found"
|
|
||||||
>("processing");
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchPaymentIntent = async () => {
|
|
||||||
if (!stripe) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { paymentIntent } = await stripe.retrievePaymentIntent(secret);
|
|
||||||
if (!paymentIntent) {
|
|
||||||
setStatus("internal_error");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const payment = await utils.pagamenti.getPaymentFromIntent.fetch({
|
|
||||||
pIntentId: paymentIntent.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!payment) {
|
|
||||||
setStatus("not_found");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (paymentIntent.status === "succeeded") {
|
|
||||||
if (payment.paymentstatus !== PaymentStatusEnum.success) {
|
|
||||||
setStatus("internal_error");
|
|
||||||
}
|
|
||||||
setStatus("success");
|
|
||||||
} else if (paymentIntent.status === "processing") {
|
|
||||||
if (payment.paymentstatus !== PaymentStatusEnum.processing) {
|
|
||||||
setStatus("internal_error");
|
|
||||||
}
|
|
||||||
setStatus("processing");
|
|
||||||
} else {
|
|
||||||
setStatus("failed");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error retrieving payment intent:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
void fetchPaymentIntent();
|
|
||||||
}, [stripe, secret]);
|
|
||||||
|
|
||||||
if (!status) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-4 p-4">
|
|
||||||
{status === "success" && (
|
|
||||||
<>
|
|
||||||
<PaymentStatus
|
|
||||||
message={t.acquisto_elaborazione.success_desc}
|
|
||||||
status="success"
|
|
||||||
title={t.acquisto_elaborazione.success_title}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ProgressRedirect href="/area-riservata/dashboard" />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{status === "processing" && (
|
|
||||||
<>
|
|
||||||
<PaymentStatus
|
|
||||||
message={t.acquisto_elaborazione.processing_desc}
|
|
||||||
status="processing"
|
|
||||||
title={t.acquisto_elaborazione.processing_title}
|
|
||||||
/>
|
|
||||||
<Link
|
|
||||||
className={cn("w-full", buttonVariants({ variant: "info" }))}
|
|
||||||
href="/area-riservata/dashboard"
|
|
||||||
>
|
|
||||||
{t.acquisto_elaborazione.to_dashboard}
|
|
||||||
</Link>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{status === "failed" && (
|
|
||||||
<>
|
|
||||||
<PaymentStatus
|
|
||||||
message={t.acquisto_elaborazione.failed_desc}
|
|
||||||
status="error"
|
|
||||||
title={t.acquisto_elaborazione.failed_title}
|
|
||||||
/>
|
|
||||||
<Link
|
|
||||||
className={cn("w-full", buttonVariants({ variant: "default" }))}
|
|
||||||
href="/area-riservata/chat"
|
|
||||||
>
|
|
||||||
{t.acquisto_elaborazione.to_chat}
|
|
||||||
</Link>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{status === "internal_error" && (
|
|
||||||
<>
|
|
||||||
<PaymentStatus
|
|
||||||
message={t.acquisto_elaborazione.internal_error_desc}
|
|
||||||
status="error"
|
|
||||||
title={t.acquisto_elaborazione.internal_error_title}
|
|
||||||
/>
|
|
||||||
<Link
|
|
||||||
className={cn("w-full", buttonVariants({ variant: "default" }))}
|
|
||||||
href="/area-riservata/chat"
|
|
||||||
>
|
|
||||||
{t.acquisto_elaborazione.to_chat}
|
|
||||||
</Link>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{status === "not_found" && (
|
|
||||||
<>
|
|
||||||
<PaymentStatus
|
|
||||||
message={t.acquisto_elaborazione.not_found_desc}
|
|
||||||
status="error"
|
|
||||||
title={t.acquisto_elaborazione.not_found_title}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
className="w-full"
|
|
||||||
onClick={() => router.back()}
|
|
||||||
variant="secondary"
|
|
||||||
>
|
|
||||||
<ArrowLeft />
|
|
||||||
{t.indietro}
|
|
||||||
</Button>
|
|
||||||
<Link
|
|
||||||
className={cn("w-full", buttonVariants({ variant: "default" }))}
|
|
||||||
href="/area-riservata/chat"
|
|
||||||
>
|
|
||||||
{t.acquisto_elaborazione.to_chat}
|
|
||||||
</Link>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -0,0 +1,117 @@
|
||||||
|
import type { GetServerSideProps } from "next";
|
||||||
|
import CheckoutForm from "~/components/checkout";
|
||||||
|
import { AreaRiservataLayout } from "~/components/Layout";
|
||||||
|
import stripe from "~/lib/stripe";
|
||||||
|
import type { NextPageWithLayout } from "~/pages/_app";
|
||||||
|
import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper";
|
||||||
|
import { zPagamentoId } from "~/server/utils/zod_types";
|
||||||
|
|
||||||
|
type PagamentoPageProps = {
|
||||||
|
clientSecret: string | null;
|
||||||
|
titolo: string;
|
||||||
|
prezzo: number;
|
||||||
|
descrizione: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PagamentoPage: NextPageWithLayout<PagamentoPageProps> = ({
|
||||||
|
clientSecret,
|
||||||
|
titolo,
|
||||||
|
prezzo,
|
||||||
|
descrizione,
|
||||||
|
}: PagamentoPageProps) => {
|
||||||
|
if (!clientSecret) {
|
||||||
|
return <div>Errore nella creazione del pagamento. Riprova più tardi.</div>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="mx-auto my-5 w-full max-w-lg space-y-8" id="checkout">
|
||||||
|
<CheckoutForm
|
||||||
|
clientSecret={clientSecret}
|
||||||
|
descrizione={descrizione}
|
||||||
|
prezzo={prezzo}
|
||||||
|
titolo={titolo}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
PagamentoPage.getLayout = function getLayout(page) {
|
||||||
|
return <AreaRiservataLayout noSidebar>{page}</AreaRiservataLayout>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PagamentoPage;
|
||||||
|
|
||||||
|
export const getServerSideProps = (async (context) => {
|
||||||
|
const id = context.params?.paymentId;
|
||||||
|
console.log("Received paymentId:", id);
|
||||||
|
if (typeof id !== "string") {
|
||||||
|
console.error("Error: paymentId is not a string");
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
destination: "/500",
|
||||||
|
permanent: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = zPagamentoId.safeParse(id);
|
||||||
|
if (!parsed.success) {
|
||||||
|
console.error("Error parsing paymentId", parsed.error);
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
destination: "/500",
|
||||||
|
permanent: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const access_token = context.req.cookies.access_token;
|
||||||
|
|
||||||
|
const helper = await TrpcAuthedFetchingIstance({ access_token });
|
||||||
|
if (helper) {
|
||||||
|
const paymentData =
|
||||||
|
await helper.trpc.pagamenti.getPagamentoDataForCheckout.fetch({
|
||||||
|
paymentId: parsed.data,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { client_secret: clientSecret } = await stripe.paymentIntents.create({
|
||||||
|
amount: paymentData.amount_cent,
|
||||||
|
|
||||||
|
automatic_payment_methods: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
currency: "eur",
|
||||||
|
description: paymentData.nome_it,
|
||||||
|
metadata: {
|
||||||
|
paymentId: paymentData.id,
|
||||||
|
servizioId: paymentData.servizio_id,
|
||||||
|
userId: paymentData.userid,
|
||||||
|
},
|
||||||
|
receipt_email: paymentData.email,
|
||||||
|
});
|
||||||
|
console.log("Created payment intent with client secret:", clientSecret);
|
||||||
|
|
||||||
|
if (!clientSecret) {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
destination: "/500",
|
||||||
|
permanent: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
clientSecret,
|
||||||
|
titolo: paymentData.nome_it,
|
||||||
|
prezzo: paymentData.amount_cent,
|
||||||
|
descrizione: paymentData.desc_it,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
destination: "/500",
|
||||||
|
permanent: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}) satisfies GetServerSideProps<PagamentoPageProps>;
|
||||||
185
apps/infoalloggi/src/pages/servizio/pagamento-status.tsx
Normal file
185
apps/infoalloggi/src/pages/servizio/pagamento-status.tsx
Normal file
|
|
@ -0,0 +1,185 @@
|
||||||
|
import { ArrowLeft } from "lucide-react";
|
||||||
|
import type { GetServerSideProps } from "next";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import PaymentStatus from "~/components/payment_status";
|
||||||
|
import { ProgressRedirect } from "~/components/progress_redirect";
|
||||||
|
import { Button, buttonVariants } from "~/components/ui/button";
|
||||||
|
import stripe from "~/lib/stripe";
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
import { useTranslation } from "~/providers/I18nProvider";
|
||||||
|
import PaymentStatusEnum from "~/schemas/public/PaymentStatusEnum";
|
||||||
|
import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper";
|
||||||
|
|
||||||
|
enum AcquistoStatusEnum {
|
||||||
|
success = "success",
|
||||||
|
processing = "processing",
|
||||||
|
failed = "failed",
|
||||||
|
internal_error = "internal_error",
|
||||||
|
not_found = "not_found",
|
||||||
|
}
|
||||||
|
|
||||||
|
type AcquistoProcessingProps = {
|
||||||
|
status: AcquistoStatusEnum;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getServerSideProps = (async (context) => {
|
||||||
|
const paymentIntentId = context.query?.payment_intent;
|
||||||
|
if (!paymentIntentId || typeof paymentIntentId !== "string") {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
destination: "/500",
|
||||||
|
permanent: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const paymentIntent = await stripe.paymentIntents.retrieve(paymentIntentId);
|
||||||
|
if (!paymentIntent) {
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
status: AcquistoStatusEnum.internal_error,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const access_token = context.req.cookies.access_token;
|
||||||
|
|
||||||
|
const helper = await TrpcAuthedFetchingIstance({ access_token });
|
||||||
|
if (helper) {
|
||||||
|
const payment = await helper.trpc.pagamenti.getPaymentFromIntent.fetch({
|
||||||
|
pIntentId: paymentIntent.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!payment) {
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
status: AcquistoStatusEnum.not_found,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let resultStatus: AcquistoStatusEnum;
|
||||||
|
if (paymentIntent.status === "succeeded") {
|
||||||
|
if (payment.paymentstatus !== PaymentStatusEnum.success) {
|
||||||
|
resultStatus = AcquistoStatusEnum.internal_error;
|
||||||
|
}
|
||||||
|
resultStatus = AcquistoStatusEnum.success;
|
||||||
|
} else if (paymentIntent.status === "processing") {
|
||||||
|
if (payment.paymentstatus !== PaymentStatusEnum.processing) {
|
||||||
|
resultStatus = AcquistoStatusEnum.internal_error;
|
||||||
|
}
|
||||||
|
resultStatus = AcquistoStatusEnum.processing;
|
||||||
|
} else {
|
||||||
|
resultStatus = AcquistoStatusEnum.failed;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
status: resultStatus,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
destination: "/500",
|
||||||
|
permanent: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}) satisfies GetServerSideProps<AcquistoProcessingProps>;
|
||||||
|
|
||||||
|
export default function AcquistoProcessing({
|
||||||
|
status,
|
||||||
|
}: AcquistoProcessingProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const router = useRouter();
|
||||||
|
return (
|
||||||
|
<div className="mx-auto my-5 w-full max-w-lg space-y-8">
|
||||||
|
<h3 className="my-8 text-center font-bold text-neutral-700 text-xl uppercase tracking-wide dark:text-white">
|
||||||
|
{t.acquisto_elaborazione.titolo}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="space-y-4 p-4">
|
||||||
|
{status === AcquistoStatusEnum.success && (
|
||||||
|
<>
|
||||||
|
<PaymentStatus
|
||||||
|
message={t.acquisto_elaborazione.success_desc}
|
||||||
|
status="success"
|
||||||
|
title={t.acquisto_elaborazione.success_title}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ProgressRedirect href="/area-riservata/dashboard" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{status === AcquistoStatusEnum.processing && (
|
||||||
|
<>
|
||||||
|
<PaymentStatus
|
||||||
|
message={t.acquisto_elaborazione.processing_desc}
|
||||||
|
status="processing"
|
||||||
|
title={t.acquisto_elaborazione.processing_title}
|
||||||
|
/>
|
||||||
|
<Link
|
||||||
|
className={cn("w-full", buttonVariants({ variant: "info" }))}
|
||||||
|
href="/area-riservata/dashboard"
|
||||||
|
>
|
||||||
|
{t.acquisto_elaborazione.to_dashboard}
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{status === AcquistoStatusEnum.failed && (
|
||||||
|
<>
|
||||||
|
<PaymentStatus
|
||||||
|
message={t.acquisto_elaborazione.failed_desc}
|
||||||
|
status="error"
|
||||||
|
title={t.acquisto_elaborazione.failed_title}
|
||||||
|
/>
|
||||||
|
<Link
|
||||||
|
className={cn("w-full", buttonVariants({ variant: "default" }))}
|
||||||
|
href="/area-riservata/chat"
|
||||||
|
>
|
||||||
|
{t.acquisto_elaborazione.to_chat}
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{status === AcquistoStatusEnum.internal_error && (
|
||||||
|
<>
|
||||||
|
<PaymentStatus
|
||||||
|
message={t.acquisto_elaborazione.internal_error_desc}
|
||||||
|
status="error"
|
||||||
|
title={t.acquisto_elaborazione.internal_error_title}
|
||||||
|
/>
|
||||||
|
<Link
|
||||||
|
className={cn("w-full", buttonVariants({ variant: "default" }))}
|
||||||
|
href="/area-riservata/chat"
|
||||||
|
>
|
||||||
|
{t.acquisto_elaborazione.to_chat}
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{status === AcquistoStatusEnum.not_found && (
|
||||||
|
<>
|
||||||
|
<PaymentStatus
|
||||||
|
message={t.acquisto_elaborazione.not_found_desc}
|
||||||
|
status="error"
|
||||||
|
title={t.acquisto_elaborazione.not_found_title}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
className="w-full"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
variant="secondary"
|
||||||
|
>
|
||||||
|
<ArrowLeft />
|
||||||
|
{t.indietro}
|
||||||
|
</Button>
|
||||||
|
<Link
|
||||||
|
className={cn("w-full", buttonVariants({ variant: "default" }))}
|
||||||
|
href="/area-riservata/chat"
|
||||||
|
>
|
||||||
|
{t.acquisto_elaborazione.to_chat}
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,12 +1,16 @@
|
||||||
import { loadStripe } from "@stripe/stripe-js";
|
import { ArrowRight, ExternalLink } from "lucide-react";
|
||||||
import type { GetServerSideProps } from "next";
|
import type { GetServerSideProps } from "next";
|
||||||
import { AcquistoProcessing } from "~/components/acquisto_processing";
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
import { AreaRiservataLayout } from "~/components/Layout";
|
import { AreaRiservataLayout } from "~/components/Layout";
|
||||||
import { LoadingPage } from "~/components/loading";
|
import { LoadingPage } from "~/components/loading";
|
||||||
import { Status500 } from "~/components/status-page";
|
import { Status500 } from "~/components/status-page";
|
||||||
import { env } from "~/env";
|
import { Button } from "~/components/ui/button";
|
||||||
|
import { formatCurrency } from "~/lib/utils";
|
||||||
import type { NextPageWithLayout } from "~/pages/_app";
|
import type { NextPageWithLayout } from "~/pages/_app";
|
||||||
|
import { useTranslation } from "~/providers/I18nProvider";
|
||||||
import type { OrdiniOrdineId } from "~/schemas/public/Ordini";
|
import type { OrdiniOrdineId } from "~/schemas/public/Ordini";
|
||||||
|
import type { PrezziarioIdprezziario } from "~/schemas/public/Prezziario";
|
||||||
import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper";
|
import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper";
|
||||||
import { zOrdineId } from "~/server/utils/zod_types";
|
import { zOrdineId } from "~/server/utils/zod_types";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
|
|
@ -18,12 +22,11 @@ type ServizioPurchaseProps = {
|
||||||
const ServicePurchasePage: NextPageWithLayout<ServizioPurchaseProps> = ({
|
const ServicePurchasePage: NextPageWithLayout<ServizioPurchaseProps> = ({
|
||||||
ordineId,
|
ordineId,
|
||||||
}: ServizioPurchaseProps) => {
|
}: ServizioPurchaseProps) => {
|
||||||
|
const router = useRouter();
|
||||||
const { data, isLoading } = api.servizio.getServizioPaymentData.useQuery({
|
const { data, isLoading } = api.servizio.getServizioPaymentData.useQuery({
|
||||||
ordineId,
|
ordineId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const stripePromise = loadStripe(env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY);
|
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <LoadingPage />;
|
return <LoadingPage />;
|
||||||
}
|
}
|
||||||
|
|
@ -32,15 +35,77 @@ const ServicePurchasePage: NextPageWithLayout<ServizioPurchaseProps> = ({
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto w-full max-w-6xl grow space-y-4 p-2 sm:px-8 sm:py-4">
|
<div className="mx-auto w-full max-w-6xl grow space-y-4 p-2 sm:px-8 sm:py-4">
|
||||||
<AcquistoProcessing
|
<PricePreparation
|
||||||
|
onProcedi={async () =>
|
||||||
|
await router.push(`/servizio/pagamento-checkout/${data.payment.id}`)
|
||||||
|
}
|
||||||
packId={data.ordine.packid}
|
packId={data.ordine.packid}
|
||||||
paymentId={data.payment.id}
|
|
||||||
stripePromise={stripePromise}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const PricePreparation = ({
|
||||||
|
packId,
|
||||||
|
onProcedi,
|
||||||
|
}: {
|
||||||
|
packId: PrezziarioIdprezziario;
|
||||||
|
onProcedi: () => void;
|
||||||
|
}) => {
|
||||||
|
const { data: prezziario, isLoading } =
|
||||||
|
api.prezziario.getPrezzoPerServizio.useQuery({
|
||||||
|
idprezziario: packId,
|
||||||
|
});
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<div className="mx-auto mt-4 max-w-2xl p-2 sm:mt-10">
|
||||||
|
{isLoading || !prezziario ? (
|
||||||
|
<>
|
||||||
|
<h3 className="font-semibold text-2xl">{t.acquisto.preparazione}</h3>
|
||||||
|
<LoadingPage />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-8">
|
||||||
|
<div className="space-y-6">
|
||||||
|
<p className="text-3xl">{prezziario.nome_it}</p>
|
||||||
|
<p className="text-lg">
|
||||||
|
Cod: “{prezziario.idprezziario}”
|
||||||
|
</p>
|
||||||
|
<p className="font-bold text-4xl">
|
||||||
|
{formatCurrency(prezziario.prezzo_cent / 100)}
|
||||||
|
</p>
|
||||||
|
<p>{prezziario.desc_it}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{prezziario.testo_condizioni && (
|
||||||
|
<Link
|
||||||
|
aria-label="Leggi le condizioni"
|
||||||
|
className="block w-fit"
|
||||||
|
href={`/servizio/condizioni/${prezziario.testo_condizioni}`}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<Button className="w-full text-sm">
|
||||||
|
<span>{t.acquisto.leggi_condizioni}</span>
|
||||||
|
<ExternalLink />
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className="w-full text-lg"
|
||||||
|
disabled={isLoading || !prezziario}
|
||||||
|
onClick={onProcedi}
|
||||||
|
size="xl"
|
||||||
|
variant="info"
|
||||||
|
>
|
||||||
|
<span>{t.acquisto.procedi_pagamento}</span> <ArrowRight />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
ServicePurchasePage.getLayout = function getLayout(page) {
|
ServicePurchasePage.getLayout = function getLayout(page) {
|
||||||
return <AreaRiservataLayout noSidebar>{page}</AreaRiservataLayout>;
|
return <AreaRiservataLayout noSidebar>{page}</AreaRiservataLayout>;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
|
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
|
||||||
|
import { getPagamentoDataForCheckout } from "~/server/controllers/servizio.controller";
|
||||||
import { db } from "~/server/db";
|
import { db } from "~/server/db";
|
||||||
|
import { zPagamentoId } from "~/server/utils/zod_types";
|
||||||
// Create a new ratelimiter, that allows 10 requests per 10 seconds
|
// Create a new ratelimiter, that allows 10 requests per 10 seconds
|
||||||
/*
|
/*
|
||||||
const ratelimit = new RateLimiterHandler({
|
const ratelimit = new RateLimiterHandler({
|
||||||
|
|
@ -30,4 +32,10 @@ export const pagamentiRouter = createTRPCRouter({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
getPagamentoDataForCheckout: protectedProcedure
|
||||||
|
.input(z.object({ paymentId: zPagamentoId }))
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
return await getPagamentoDataForCheckout(input.paymentId);
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1535,6 +1535,26 @@ export const getServizioPagamentoData = async (ordineId: OrdiniOrdineId) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getPagamentoDataForCheckout = async (paymentId: PaymentsId) => {
|
||||||
|
try {
|
||||||
|
return await db
|
||||||
|
.selectFrom("payments")
|
||||||
|
.selectAll("payments")
|
||||||
|
.innerJoin("users", "users.id", "payments.userid")
|
||||||
|
.select("users.email")
|
||||||
|
.innerJoin("ordini", "ordini.ordine_id", "payments.ordine_id")
|
||||||
|
.innerJoin("prezziario", "prezziario.idprezziario", "ordini.packid")
|
||||||
|
.select(["prezziario.nome_it", "prezziario.desc_it"])
|
||||||
|
.where("payments.id", "=", paymentId)
|
||||||
|
.executeTakeFirstOrThrow();
|
||||||
|
} catch (e) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: `Payment not found: ${(e as Error).message}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
type Pagamento = Payments & Pick<Users, "username">;
|
type Pagamento = Payments & Pick<Users, "username">;
|
||||||
export const getAllPagamenti = async (): Promise<Pagamento[]> => {
|
export const getAllPagamenti = async (): Promise<Pagamento[]> => {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,20 @@ const stripeApi = new Stripe(env.STRIPE_SECRET_KEY);
|
||||||
export const stripeHealthCheck = async () => {
|
export const stripeHealthCheck = async () => {
|
||||||
try {
|
try {
|
||||||
const endpoints = await stripeApi.webhookEndpoints.list();
|
const endpoints = await stripeApi.webhookEndpoints.list();
|
||||||
return { success: true, endpoints };
|
return {
|
||||||
|
success: true,
|
||||||
|
endpoints,
|
||||||
|
secretKey: env.STRIPE_SECRET_KEY,
|
||||||
|
publicKey: env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY,
|
||||||
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Stripe health check failed:", e);
|
console.error("Stripe health check failed:", e);
|
||||||
return { success: false, endpoints: null };
|
return {
|
||||||
|
success: false,
|
||||||
|
endpoints: null,
|
||||||
|
secretKey: env.STRIPE_SECRET_KEY,
|
||||||
|
publicKey: env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue