Several fixes
parent
1bdf1ce081
commit
7aafceb87d
|
@ -94,7 +94,7 @@ const querySchema = z.object({
|
|||
});
|
||||
|
||||
export default function Success(props: SuccessProps) {
|
||||
const { t, i18n } = useLocale();
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const routerQuery = useRouterQuery();
|
||||
const pathname = usePathname();
|
||||
|
|
|
@ -114,7 +114,7 @@ export default function AppCard({
|
|||
{app?.isInstalled && switchChecked && <hr className="border-subtle" />}
|
||||
|
||||
{app?.isInstalled && switchChecked ? (
|
||||
app.isSetup ? (
|
||||
app.isSetupAlready ? (
|
||||
<div className="relative p-4 pt-5 text-sm [&_input]:mb-0 [&_input]:leading-4">
|
||||
<Link href={`/apps/${app.slug}/setup`} className="absolute right-4 top-4">
|
||||
<Settings className="text-default h-4 w-4" aria-hidden="true" />
|
||||
|
|
|
@ -4,7 +4,6 @@ import prisma from "@calcom/prisma";
|
|||
|
||||
import config from "../config.json";
|
||||
|
||||
// FIXME: is a custom handler really needed?
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (!req.session?.user?.id) {
|
||||
return res.status(401).json({ message: "You must be logged in to do this" });
|
||||
|
@ -41,19 +40,3 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
|
||||
return res.status(200).json({ url: "/apps/alby/setup" });
|
||||
}
|
||||
/*import { createDefaultInstallation } from "@calcom/app-store/_utils/installation";
|
||||
import type { AppDeclarativeHandler } from "@calcom/types/AppHandler";
|
||||
|
||||
import appConfig from "../config.json";
|
||||
|
||||
const handler: AppDeclarativeHandler = {
|
||||
appType: appConfig.type,
|
||||
variant: appConfig.variant,
|
||||
slug: appConfig.slug,
|
||||
supportsMultipleInstalls: false,
|
||||
handlerType: "add",
|
||||
createCredential: ({ appType, user, slug, teamId }) =>
|
||||
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
|
||||
};
|
||||
|
||||
export default handler;*/
|
||||
|
|
|
@ -123,9 +123,6 @@ export const AlbyPaymentComponent = (props: IAlbyPaymentComponentProps) => {
|
|||
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();
|
||||
|
@ -137,22 +134,17 @@ function PaymentChecker(props: PaymentCheckerProps) {
|
|||
if (props.booking.status === "ACCEPTED") {
|
||||
return;
|
||||
}
|
||||
const bookingsResult = await utils.viewer.bookings.get.fetch({
|
||||
filters: {
|
||||
status: "upcoming",
|
||||
eventTypeIds: [props.eventType.id],
|
||||
},
|
||||
const bookingsResult = await utils.viewer.bookings.find.fetch({
|
||||
bookingUid: props.booking.uid,
|
||||
});
|
||||
if (
|
||||
bookingsResult.bookings.some(
|
||||
(booking) => booking.id === props.booking.id && booking.status === "ACCEPTED"
|
||||
)
|
||||
) {
|
||||
|
||||
if (bookingsResult.paid) {
|
||||
showToast("Payment successful", "success");
|
||||
|
||||
// TODO: add typings here
|
||||
const params: {
|
||||
[k: string]: any;
|
||||
uid: string;
|
||||
email: string | null;
|
||||
location: string;
|
||||
} = {
|
||||
uid: props.booking.uid,
|
||||
email: searchParams.get("email"),
|
||||
|
|
|
@ -10,10 +10,15 @@ import type { IAbstractPaymentService } from "@calcom/types/PaymentService";
|
|||
import { albyCredentialKeysSchema } from "./albyCredentialKeysSchema";
|
||||
|
||||
export class PaymentService implements IAbstractPaymentService {
|
||||
private credentials: z.infer<typeof albyCredentialKeysSchema>;
|
||||
private credentials: z.infer<typeof albyCredentialKeysSchema> | null;
|
||||
|
||||
constructor(credentials: { key: Prisma.JsonValue }) {
|
||||
this.credentials = albyCredentialKeysSchema.parse(credentials.key);
|
||||
const keyParsing = albyCredentialKeysSchema.safeParse(credentials.key);
|
||||
if (keyParsing.success) {
|
||||
this.credentials = keyParsing.data;
|
||||
} else {
|
||||
this.credentials = null;
|
||||
}
|
||||
}
|
||||
|
||||
async create(
|
||||
|
@ -30,7 +35,7 @@ export class PaymentService implements IAbstractPaymentService {
|
|||
id: bookingId,
|
||||
},
|
||||
});
|
||||
if (!booking) {
|
||||
if (!booking || !this.credentials?.account_lightning_address) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
|
@ -121,4 +126,8 @@ export class PaymentService implements IAbstractPaymentService {
|
|||
deletePayment(paymentId: number): Promise<boolean> {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
isSetupAlready(): boolean {
|
||||
return !!this.credentials;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,13 @@ export default function parseInvoice(
|
|||
"svix-signature": string;
|
||||
},
|
||||
webhookEndpointSecret: string
|
||||
): Invoice {
|
||||
const wh = new Webhook(webhookEndpointSecret);
|
||||
return wh.verify(body, headers) as Invoice;
|
||||
): Invoice | null {
|
||||
try {
|
||||
const wh = new Webhook(webhookEndpointSecret);
|
||||
return wh.verify(body, headers) as Invoice;
|
||||
} catch (err) {
|
||||
// Looks like alby might sent multiple webhooks for the same invoice but it should only work once
|
||||
console.error(err);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -17,10 +17,15 @@ export const paypalCredentialKeysSchema = z.object({
|
|||
});
|
||||
|
||||
export class PaymentService implements IAbstractPaymentService {
|
||||
private credentials: z.infer<typeof paypalCredentialKeysSchema>;
|
||||
private credentials: z.infer<typeof paypalCredentialKeysSchema> | null;
|
||||
|
||||
constructor(credentials: { key: Prisma.JsonValue }) {
|
||||
this.credentials = paypalCredentialKeysSchema.parse(credentials.key);
|
||||
const keyParsing = paypalCredentialKeysSchema.safeParse(credentials.key);
|
||||
if (keyParsing.success) {
|
||||
this.credentials = keyParsing.data;
|
||||
} else {
|
||||
this.credentials = null;
|
||||
}
|
||||
}
|
||||
|
||||
async create(
|
||||
|
@ -37,7 +42,7 @@ export class PaymentService implements IAbstractPaymentService {
|
|||
id: bookingId,
|
||||
},
|
||||
});
|
||||
if (!booking) {
|
||||
if (!booking || !this.credentials) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
|
@ -113,7 +118,7 @@ export class PaymentService implements IAbstractPaymentService {
|
|||
id: bookingId,
|
||||
},
|
||||
});
|
||||
if (!booking) {
|
||||
if (!booking || !this.credentials) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
|
@ -192,4 +197,8 @@ export class PaymentService implements IAbstractPaymentService {
|
|||
deletePayment(paymentId: number): Promise<boolean> {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
isSetupAlready(): boolean {
|
||||
return !!this.credentials;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ import { createPaymentLink } from "./client";
|
|||
import { retrieveOrCreateStripeCustomerByEmail } from "./customer";
|
||||
import type { StripePaymentData, StripeSetupIntentData } from "./server";
|
||||
|
||||
const stripeCredentialKeysSchema = z.object({
|
||||
export const stripeCredentialKeysSchema = z.object({
|
||||
stripe_user_id: z.string(),
|
||||
default_currency: z.string(),
|
||||
stripe_publishable_key: z.string(),
|
||||
|
@ -28,11 +28,15 @@ const stripeAppKeysSchema = z.object({
|
|||
|
||||
export class PaymentService implements IAbstractPaymentService {
|
||||
private stripe: Stripe;
|
||||
private credentials: z.infer<typeof stripeCredentialKeysSchema>;
|
||||
private credentials: z.infer<typeof stripeCredentialKeysSchema> | null;
|
||||
|
||||
constructor(credentials: { key: Prisma.JsonValue }) {
|
||||
// parse credentials key
|
||||
this.credentials = stripeCredentialKeysSchema.parse(credentials.key);
|
||||
const keyParsing = stripeCredentialKeysSchema.safeParse(credentials.key);
|
||||
if (keyParsing.success) {
|
||||
this.credentials = keyParsing.data;
|
||||
} else {
|
||||
this.credentials = null;
|
||||
}
|
||||
this.stripe = new Stripe(process.env.STRIPE_PRIVATE_KEY || "", {
|
||||
apiVersion: "2020-08-27",
|
||||
});
|
||||
|
@ -63,15 +67,9 @@ export class PaymentService implements IAbstractPaymentService {
|
|||
throw new Error("Payment option is not compatible with create method");
|
||||
}
|
||||
|
||||
// Load stripe keys
|
||||
const stripeAppKeys = await prisma.app.findFirst({
|
||||
select: {
|
||||
keys: true,
|
||||
},
|
||||
where: {
|
||||
slug: "stripe",
|
||||
},
|
||||
});
|
||||
if (!this.credentials) {
|
||||
throw new Error("Stripe credentials not found");
|
||||
}
|
||||
|
||||
const customer = await retrieveOrCreateStripeCustomerByEmail(
|
||||
bookerEmail,
|
||||
|
@ -142,21 +140,15 @@ export class PaymentService implements IAbstractPaymentService {
|
|||
paymentOption: PaymentOption
|
||||
): Promise<Payment> {
|
||||
try {
|
||||
if (!this.credentials) {
|
||||
throw new Error("Stripe credentials not found");
|
||||
}
|
||||
|
||||
// Ensure that the payment service can support the passed payment option
|
||||
if (paymentOptionEnum.parse(paymentOption) !== "HOLD") {
|
||||
throw new Error("Payment option is not compatible with create method");
|
||||
}
|
||||
|
||||
// Load stripe keys
|
||||
const stripeAppKeys = await prisma.app.findFirst({
|
||||
select: {
|
||||
keys: true,
|
||||
},
|
||||
where: {
|
||||
slug: "stripe",
|
||||
},
|
||||
});
|
||||
|
||||
const customer = await retrieveOrCreateStripeCustomerByEmail(
|
||||
bookerEmail,
|
||||
this.credentials.stripe_user_id
|
||||
|
@ -214,6 +206,10 @@ export class PaymentService implements IAbstractPaymentService {
|
|||
|
||||
async chargeCard(payment: Payment, _bookingId?: Booking["id"]): Promise<Payment> {
|
||||
try {
|
||||
if (!this.credentials) {
|
||||
throw new Error("Stripe credentials not found");
|
||||
}
|
||||
|
||||
const stripeAppKeys = await prisma.app.findFirst({
|
||||
select: {
|
||||
keys: true,
|
||||
|
@ -385,4 +381,8 @@ export class PaymentService implements IAbstractPaymentService {
|
|||
getPaymentDetails(): Promise<Payment> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
isSetupAlready(): boolean {
|
||||
return !!this.credentials;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -83,7 +83,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
const credentials = parseCredentials.data;
|
||||
|
||||
const albyInvoice = await parseInvoice(bodyAsString, parsedHeaders, credentials.webhook_endpoint_secret);
|
||||
|
||||
if (!albyInvoice) throw new HttpCode({ statusCode: 204, message: "Invoice not found" });
|
||||
if (albyInvoice.amount !== payment.amount) {
|
||||
throw new HttpCode({ statusCode: 400, message: "invoice amount does not match payment amount" });
|
||||
}
|
||||
|
|
|
@ -47,6 +47,7 @@ const getEnabledAppsFromCredentials = async (
|
|||
..._where,
|
||||
...(filterOnIds.credentials.some.OR.length && filterOnIds),
|
||||
};
|
||||
|
||||
const enabledApps = await prisma.app.findMany({
|
||||
where,
|
||||
select: { slug: true, enabled: true },
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import type { Prisma } from "@prisma/client";
|
||||
|
||||
import appStore from "@calcom/app-store";
|
||||
import type { CredentialOwner } from "@calcom/app-store/types";
|
||||
import getEnabledAppsFromCredentials from "@calcom/lib/apps/getEnabledAppsFromCredentials";
|
||||
import getInstallCountPerApp from "@calcom/lib/apps/getInstallCountPerApp";
|
||||
|
@ -9,6 +10,7 @@ import { MembershipRole } from "@calcom/prisma/enums";
|
|||
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
|
||||
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||
import type { CredentialPayload } from "@calcom/types/Credential";
|
||||
import type { PaymentApp } from "@calcom/types/PaymentService";
|
||||
|
||||
import type { TIntegrationsInputSchema } from "./integrations.schema";
|
||||
|
||||
|
@ -132,34 +134,48 @@ export const integrationsHandler = async ({ ctx, input }: IntegrationsOptions) =
|
|||
...(appId ? { where: { slug: appId } } : {}),
|
||||
});
|
||||
//TODO: Refactor this to pick up only needed fields and prevent more leaking
|
||||
let apps = enabledApps.map(
|
||||
({ credentials: _, credential, key: _2 /* don't leak to frontend */, ...app }) => {
|
||||
let apps = await Promise.all(
|
||||
enabledApps.map(async ({ credentials: _, credential, key: _2 /* don't leak to frontend */, ...app }) => {
|
||||
const userCredentialIds = credentials.filter((c) => c.type === app.type && !c.teamId).map((c) => c.id);
|
||||
const invalidCredentialIds = credentials
|
||||
.filter((c) => c.type === app.type && c.invalid)
|
||||
.map((c) => c.id);
|
||||
const teams = credentials
|
||||
.filter((c) => c.type === app.type && c.teamId)
|
||||
.map((c) => {
|
||||
const team = userTeams.find((team) => team.id === c.teamId);
|
||||
if (!team) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
teamId: team.id,
|
||||
name: team.name,
|
||||
logo: team.logo,
|
||||
credentialId: c.id,
|
||||
isAdmin:
|
||||
team.members[0].role === MembershipRole.ADMIN || team.members[0].role === MembershipRole.OWNER,
|
||||
};
|
||||
});
|
||||
const teams = await Promise.all(
|
||||
credentials
|
||||
.filter((c) => c.type === app.type && c.teamId)
|
||||
.map(async (c) => {
|
||||
const team = userTeams.find((team) => team.id === c.teamId);
|
||||
if (!team) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
teamId: team.id,
|
||||
name: team.name,
|
||||
logo: team.logo,
|
||||
credentialId: c.id,
|
||||
isAdmin:
|
||||
team.members[0].role === MembershipRole.ADMIN ||
|
||||
team.members[0].role === MembershipRole.OWNER,
|
||||
};
|
||||
})
|
||||
);
|
||||
// type infer as CredentialOwner
|
||||
const credentialOwner: CredentialOwner = {
|
||||
name: user.name,
|
||||
avatar: user.avatar,
|
||||
};
|
||||
|
||||
// We need to know if app is payment type
|
||||
let isSetupAlready = false;
|
||||
if (credential && app.categories.includes("payment")) {
|
||||
const paymentApp = (await appStore[app.dirName as keyof typeof appStore]()) as PaymentApp | null;
|
||||
if (paymentApp && "lib" in paymentApp && paymentApp?.lib && "PaymentService" in paymentApp?.lib) {
|
||||
const PaymentService = paymentApp.lib.PaymentService;
|
||||
const paymentInstance = new PaymentService(credential);
|
||||
isSetupAlready = paymentInstance.isSetupAlready();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...app,
|
||||
...(teams.length && {
|
||||
|
@ -170,9 +186,9 @@ export const integrationsHandler = async ({ ctx, input }: IntegrationsOptions) =
|
|||
teams,
|
||||
isInstalled: !!userCredentialIds.length || !!teams.length || app.isGlobal,
|
||||
// FIXME: remove hardcoding and add per-app validation
|
||||
isSetup: !!(credential?.key as { account_id: string })?.account_id || app.slug !== "alby",
|
||||
isSetupAlready,
|
||||
};
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
if (variant) {
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import authedProcedure from "../../../procedures/authedProcedure";
|
||||
import publicProcedure from "../../../procedures/publicProcedure";
|
||||
import { router } from "../../../trpc";
|
||||
import { ZConfirmInputSchema } from "./confirm.schema";
|
||||
import { ZEditLocationInputSchema } from "./editLocation.schema";
|
||||
import { ZFindInputSchema } from "./find.schema";
|
||||
import { ZGetInputSchema } from "./get.schema";
|
||||
import { ZGetBookingAttendeesInputSchema } from "./getBookingAttendees.schema";
|
||||
import { ZRequestRescheduleInputSchema } from "./requestReschedule.schema";
|
||||
|
@ -105,4 +107,20 @@ export const bookingsRouter = router({
|
|||
input,
|
||||
});
|
||||
}),
|
||||
|
||||
find: publicProcedure.input(ZFindInputSchema).query(async ({ input, ctx }) => {
|
||||
if (!UNSTABLE_HANDLER_CACHE.find) {
|
||||
UNSTABLE_HANDLER_CACHE.find = await import("./find.handler").then((mod) => mod.getHandler);
|
||||
}
|
||||
|
||||
// Unreachable code but required for type safety
|
||||
if (!UNSTABLE_HANDLER_CACHE.find) {
|
||||
throw new Error("Failed to load handler");
|
||||
}
|
||||
|
||||
return UNSTABLE_HANDLER_CACHE.find({
|
||||
ctx,
|
||||
input,
|
||||
});
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
import type { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
import type { TFindInputSchema } from "./find.schema";
|
||||
|
||||
type GetOptions = {
|
||||
ctx: {
|
||||
prisma: PrismaClient;
|
||||
};
|
||||
input: TFindInputSchema;
|
||||
};
|
||||
|
||||
export const getHandler = async ({ ctx, input }: GetOptions) => {
|
||||
const { prisma } = ctx;
|
||||
const { bookingUid } = input;
|
||||
|
||||
const booking = await prisma.booking.findUnique({
|
||||
where: {
|
||||
uid: bookingUid,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
uid: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
description: true,
|
||||
status: true,
|
||||
paid: true,
|
||||
eventTypeId: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Don't leak anything private from the booking
|
||||
return {
|
||||
booking,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,9 @@
|
|||
import { z } from "zod";
|
||||
|
||||
const ZFindInputSchema = z.object({
|
||||
bookingUid: z.string().optional(),
|
||||
});
|
||||
|
||||
type TFindInputSchema = z.infer<typeof ZFindInputSchema>;
|
||||
|
||||
export { ZFindInputSchema, TFindInputSchema };
|
|
@ -1,10 +1,11 @@
|
|||
import type { Payment, Prisma, Booking, PaymentOption } from "@prisma/client";
|
||||
|
||||
import type { PaymentService } from "@calcom/app-store/paypal/lib/PaymentService";
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
|
||||
export interface PaymentApp {
|
||||
lib?: {
|
||||
PaymentService: IAbstractPaymentService;
|
||||
PaymentService: typeof PaymentService;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -32,6 +33,7 @@ export interface IAbstractPaymentService {
|
|||
payment: Pick<Prisma.PaymentUncheckedCreateInput, "amount" | "currency">,
|
||||
bookingId?: Booking["id"]
|
||||
): Promise<Payment>;
|
||||
|
||||
update(paymentId: Payment["id"], data: Partial<Prisma.PaymentUncheckedCreateInput>): Promise<Payment>;
|
||||
refund(paymentId: Payment["id"]): Promise<Payment>;
|
||||
getPaymentPaidStatus(): Promise<string>;
|
||||
|
@ -47,4 +49,5 @@ export interface IAbstractPaymentService {
|
|||
paymentData: Payment
|
||||
): Promise<void>;
|
||||
deletePayment(paymentId: Payment["id"]): Promise<boolean>;
|
||||
isSetupAlready(): boolean;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue