feat: listen for successful lightning payment
parent
a76d1b02ef
commit
c4dd002c16
|
@ -1,10 +1,16 @@
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useState } from "react";
|
import { useSearchParams } from "next/navigation";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
import QRCode from "react-qr-code";
|
import QRCode from "react-qr-code";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
|
|
||||||
|
import type { PaymentPageProps } from "@calcom/features/ee/payments/pages/payment";
|
||||||
|
import { useBookingSuccessRedirect } from "@calcom/lib/bookingSuccessRedirect";
|
||||||
import { useCopy } from "@calcom/lib/hooks/useCopy";
|
import { useCopy } from "@calcom/lib/hooks/useCopy";
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Button } from "@calcom/ui";
|
import { Button } from "@calcom/ui";
|
||||||
|
import { showToast } from "@calcom/ui";
|
||||||
import { ClipboardCheck, Clipboard } from "@calcom/ui/components/icon";
|
import { ClipboardCheck, Clipboard } from "@calcom/ui/components/icon";
|
||||||
import { Spinner } from "@calcom/ui/components/icon/Spinner";
|
import { Spinner } from "@calcom/ui/components/icon/Spinner";
|
||||||
|
|
||||||
|
@ -13,6 +19,7 @@ interface IAlbyPaymentComponentProps {
|
||||||
// Will be parsed on render
|
// Will be parsed on render
|
||||||
data: unknown;
|
data: unknown;
|
||||||
};
|
};
|
||||||
|
paymentPageProps: PaymentPageProps;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create zod schema for data
|
// Create zod schema for data
|
||||||
|
@ -21,8 +28,7 @@ const PaymentAlbyDataSchema = z.object({
|
||||||
.object({
|
.object({
|
||||||
paymentRequest: z.string(),
|
paymentRequest: z.string(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.required(),
|
||||||
capture: z.object({}).optional(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const AlbyPaymentComponent = (props: IAlbyPaymentComponentProps) => {
|
export const AlbyPaymentComponent = (props: IAlbyPaymentComponentProps) => {
|
||||||
|
@ -45,6 +51,7 @@ export const AlbyPaymentComponent = (props: IAlbyPaymentComponentProps) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-4 mt-8 flex h-full w-full flex-col items-center justify-center gap-4">
|
<div className="mb-4 mt-8 flex h-full w-full flex-col items-center justify-center gap-4">
|
||||||
|
<PaymentChecker {...props.paymentPageProps} />
|
||||||
{isPaying && <Spinner className="mt-12 h-8 w-8" />}
|
{isPaying && <Spinner className="mt-12 h-8 w-8" />}
|
||||||
{!isPaying && (
|
{!isPaying && (
|
||||||
<>
|
<>
|
||||||
|
@ -58,6 +65,9 @@ export const AlbyPaymentComponent = (props: IAlbyPaymentComponentProps) => {
|
||||||
<Button
|
<Button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
try {
|
try {
|
||||||
|
if (!window.webln) {
|
||||||
|
throw new Error("webln not found");
|
||||||
|
}
|
||||||
setPaying(true);
|
setPaying(true);
|
||||||
await window.webln.enable();
|
await window.webln.enable();
|
||||||
window.webln.sendPayment(paymentRequest);
|
window.webln.sendPayment(paymentRequest);
|
||||||
|
@ -73,11 +83,15 @@ export const AlbyPaymentComponent = (props: IAlbyPaymentComponentProps) => {
|
||||||
)}
|
)}
|
||||||
{showQRCode && (
|
{showQRCode && (
|
||||||
<>
|
<>
|
||||||
|
<div className="flex items-center justify-center gap-2">
|
||||||
|
<p className="text-xs">Waiting for payment...</p>
|
||||||
|
<Spinner className="h-4 w-4" />
|
||||||
|
</div>
|
||||||
<p className="text-sm">Click or scan the invoice below to pay</p>
|
<p className="text-sm">Click or scan the invoice below to pay</p>
|
||||||
<Link
|
<Link
|
||||||
href={`lightning:${paymentRequest}`}
|
href={`lightning:${paymentRequest}`}
|
||||||
className="inline-flex items-center justify-center rounded-2xl rounded-md border border-transparent p-2
|
className="inline-flex items-center justify-center rounded-2xl rounded-md border border-transparent p-2
|
||||||
font-medium text-black shadow-sm hover:brightness-95 focus:outline-none focus:ring-offset-2">
|
font-medium text-black shadow-sm hover:brightness-95 focus:outline-none focus:ring-offset-2">
|
||||||
<QRCode size={128} value={paymentRequest} />
|
<QRCode size={128} value={paymentRequest} />
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
|
@ -100,8 +114,73 @@ export const AlbyPaymentComponent = (props: IAlbyPaymentComponentProps) => {
|
||||||
<div className="mt-4 flex items-center text-sm">
|
<div className="mt-4 flex items-center text-sm">
|
||||||
Powered by
|
Powered by
|
||||||
<img title="Alby" src="/app-store/alby/icon.svg" alt="Alby" className="h-8 w-8" />
|
<img title="Alby" src="/app-store/alby/icon.svg" alt="Alby" className="h-8 w-8" />
|
||||||
|
Alby
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type PaymentCheckerProps = PaymentPageProps;
|
||||||
|
|
||||||
|
function PaymentChecker(props: PaymentCheckerProps) {
|
||||||
|
// This effect checks if the booking status has changed to "ACCEPTED"
|
||||||
|
// then reload the page to show the new payment status
|
||||||
|
// FIXME: subscribe to the exact payment instead of polling bookings
|
||||||
|
// FIXME: booking success is copied from packages/features/ee/payments/components/Payment.tsx
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const bookingSuccessRedirect = useBookingSuccessRedirect();
|
||||||
|
const utils = trpc.useContext();
|
||||||
|
const { t } = useLocale();
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
(async () => {
|
||||||
|
if (props.booking.status === "ACCEPTED") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const bookingsResult = await utils.viewer.bookings.get.fetch({
|
||||||
|
filters: {
|
||||||
|
status: "upcoming",
|
||||||
|
eventTypeIds: [props.eventType.id],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// TODO: is there a better way than reloading the whole page?
|
||||||
|
// currently props.booking comes from SSR
|
||||||
|
if (
|
||||||
|
bookingsResult.bookings.some(
|
||||||
|
(booking) => booking.id === props.booking.id && booking.status === "ACCEPTED"
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
showToast("Payment successful", "success");
|
||||||
|
|
||||||
|
const params: {
|
||||||
|
[k: string]: any;
|
||||||
|
} = {
|
||||||
|
uid: props.booking.uid,
|
||||||
|
email: searchParams.get("email"),
|
||||||
|
location: t("web_conferencing_details_to_follow"),
|
||||||
|
};
|
||||||
|
|
||||||
|
bookingSuccessRedirect({
|
||||||
|
successRedirectUrl: props.eventType.successRedirectUrl,
|
||||||
|
query: params,
|
||||||
|
booking: props.booking,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, 1000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [
|
||||||
|
bookingSuccessRedirect,
|
||||||
|
props.booking,
|
||||||
|
props.booking.id,
|
||||||
|
props.booking.status,
|
||||||
|
props.eventType.id,
|
||||||
|
props.eventType.successRedirectUrl,
|
||||||
|
props.payment.success,
|
||||||
|
searchParams,
|
||||||
|
t,
|
||||||
|
utils.viewer.bookings,
|
||||||
|
]);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
|
@ -152,7 +152,7 @@ const PaymentPage: FC<PaymentPageProps> = (props) => {
|
||||||
<PaypalPaymentComponent payment={props.payment} />
|
<PaypalPaymentComponent payment={props.payment} />
|
||||||
)}
|
)}
|
||||||
{props.payment.appId === "alby" && !props.payment.success && (
|
{props.payment.appId === "alby" && !props.payment.success && (
|
||||||
<AlbyPaymentComponent payment={props.payment} />
|
<AlbyPaymentComponent payment={props.payment} paymentPageProps={props} />
|
||||||
)}
|
)}
|
||||||
{props.payment.refunded && (
|
{props.payment.refunded && (
|
||||||
<div className="text-default mt-4 text-center dark:text-gray-300">{t("refunded")}</div>
|
<div className="text-default mt-4 text-center dark:text-gray-300">{t("refunded")}</div>
|
||||||
|
|
Loading…
Reference in New Issue