Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d18a09cc8 | ||
|
|
887d63ceef | ||
|
|
555fcc21f2 | ||
|
|
f45fb03838 | ||
|
|
46611fdf77 | ||
|
|
c0f7c40bec | ||
|
|
f6f257c705 | ||
|
|
d61f50724b | ||
|
|
960aa0e00b | ||
|
|
817e20f11e | ||
|
|
af0123a606 | ||
|
|
709bbf6626 | ||
|
|
53097ee3bc | ||
|
|
31f2002654 | ||
|
|
ee155c3643 | ||
|
|
8ab20c3c9f | ||
|
|
4f6d035d29 | ||
|
|
2e5b42ef47 | ||
|
|
b055672ea0 | ||
|
|
e4fbca388c | ||
|
|
2b8bba09c9 | ||
|
|
98cf29f655 | ||
|
|
ae8706316f | ||
|
|
ec8e08d442 | ||
|
|
c33f424333 | ||
|
|
6f2312d1e2 |
@@ -15,6 +15,9 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/stale@v7
|
||||
with:
|
||||
days-before-stale: 60
|
||||
include-only-assigned: true
|
||||
days-before-close: -1
|
||||
days-before-issue-stale: 60
|
||||
days-before-issue-close: -1
|
||||
days-before-pr-stale: 14
|
||||
days-before-pr-close: 7
|
||||
stale-pr-message: "This PR is being marked as stale due to inactivity."
|
||||
close-pr-message: "This PR is being closed due to inactivity. Please reopen if work is intended to be continued.
|
||||
|
||||
@@ -132,9 +132,9 @@ const BookingDescription: FC<Props> = (props) => {
|
||||
variant="gray"
|
||||
className={classNames(
|
||||
duration === dur.toString()
|
||||
? "bg-darkgray-200 text-darkgray-900 dark:bg-darkmodebrand dark:!text-darkmodebrandcontrast"
|
||||
: "hover:bg-darkgray-200 dark:hover:bg-darkmodebrand hover:text-darkgray-900 dark:hover:text-darkmodebrandcontrast dark:bg-darkgray-200 bg-emphasis text-emphasis dark:text-inverted",
|
||||
"cursor-pointer"
|
||||
? "bg-emphasis border-emphasis text-emphasis "
|
||||
: "bg-subtle text-default border-transparent ",
|
||||
"cursor-pointer border"
|
||||
)}
|
||||
onClick={() => {
|
||||
setDuration(dur);
|
||||
|
||||
@@ -1,22 +1,35 @@
|
||||
import type { TFunction } from "next-i18next";
|
||||
import { FormattedNumber, IntlProvider } from "react-intl";
|
||||
|
||||
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
|
||||
import { FiCreditCard } from "@calcom/ui/components/icon";
|
||||
|
||||
const BookingDescriptionPayment = (props: { eventType: Parameters<typeof getPaymentAppData>[0] }) => {
|
||||
const BookingDescriptionPayment = (props: {
|
||||
eventType: Parameters<typeof getPaymentAppData>[0];
|
||||
t: TFunction;
|
||||
}) => {
|
||||
const paymentAppData = getPaymentAppData(props.eventType);
|
||||
if (!paymentAppData || paymentAppData.price <= 0) return null;
|
||||
|
||||
return (
|
||||
<p className="text-bookinglight -ml-2 px-2 text-sm ">
|
||||
<FiCreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||
<IntlProvider locale="en">
|
||||
<FormattedNumber
|
||||
value={paymentAppData.price / 100.0}
|
||||
style="currency"
|
||||
currency={paymentAppData.currency?.toUpperCase()}
|
||||
/>
|
||||
</IntlProvider>
|
||||
{paymentAppData.paymentOption === "HOLD" ? (
|
||||
<>
|
||||
{props.t("no_show_fee_amount", {
|
||||
amount: paymentAppData.price / 100.0,
|
||||
formatParams: { amount: { currency: paymentAppData.currency } },
|
||||
})}
|
||||
</>
|
||||
) : (
|
||||
<IntlProvider locale="en">
|
||||
<FormattedNumber
|
||||
value={paymentAppData.price / 100.0}
|
||||
style="currency"
|
||||
currency={paymentAppData.currency?.toUpperCase()}
|
||||
/>
|
||||
</IntlProvider>
|
||||
)}
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -28,10 +28,20 @@ import {
|
||||
TableActions,
|
||||
TextAreaField,
|
||||
} from "@calcom/ui";
|
||||
import { FiCheck, FiClock, FiMapPin, FiRefreshCcw, FiSend, FiSlash, FiX } from "@calcom/ui/components/icon";
|
||||
import {
|
||||
FiCheck,
|
||||
FiClock,
|
||||
FiMapPin,
|
||||
FiRefreshCcw,
|
||||
FiSend,
|
||||
FiSlash,
|
||||
FiX,
|
||||
FiCreditCard,
|
||||
} from "@calcom/ui/components/icon";
|
||||
|
||||
import useMeQuery from "@lib/hooks/useMeQuery";
|
||||
|
||||
import { ChargeCardDialog } from "@components/dialog/ChargeCardDialog";
|
||||
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
|
||||
import { RescheduleDialog } from "@components/dialog/RescheduleDialog";
|
||||
|
||||
@@ -56,7 +66,9 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
const router = useRouter();
|
||||
const [rejectionReason, setRejectionReason] = useState<string>("");
|
||||
const [rejectionDialogIsOpen, setRejectionDialogIsOpen] = useState(false);
|
||||
const [chargeCardDialogIsOpen, setChargeCardDialogIsOpen] = useState(false);
|
||||
const [viewRecordingsDialogIsOpen, setViewRecordingsDialogIsOpen] = useState<boolean>(false);
|
||||
const cardCharged = booking?.payment[0]?.success;
|
||||
const mutation = trpc.viewer.bookings.confirm.useMutation({
|
||||
onSuccess: (data) => {
|
||||
if (data?.status === BookingStatus.REJECTED) {
|
||||
@@ -116,16 +128,20 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
icon: FiSlash,
|
||||
disabled: mutation.isLoading,
|
||||
},
|
||||
{
|
||||
id: "confirm",
|
||||
label: (isTabRecurring || isTabUnconfirmed) && isRecurring ? t("confirm_all") : t("confirm"),
|
||||
onClick: () => {
|
||||
bookingConfirm(true);
|
||||
},
|
||||
icon: FiCheck,
|
||||
disabled: mutation.isLoading,
|
||||
color: "primary",
|
||||
},
|
||||
// For bookings with payment, only confirm if the booking is paid for
|
||||
...((isPending && !booking?.eventType?.price) || (!!booking?.eventType?.price && booking.paid)
|
||||
? [
|
||||
{
|
||||
id: "confirm",
|
||||
label: (isTabRecurring || isTabUnconfirmed) && isRecurring ? t("confirm_all") : t("confirm"),
|
||||
onClick: () => {
|
||||
bookingConfirm(true);
|
||||
},
|
||||
icon: FiCheck,
|
||||
disabled: mutation.isLoading,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
const showRecordingActions: ActionType[] = [
|
||||
@@ -184,6 +200,18 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
},
|
||||
];
|
||||
|
||||
const chargeCardActions: ActionType[] = [
|
||||
{
|
||||
id: "charge_card",
|
||||
label: cardCharged ? t("no_show_fee_charged") : t("collect_no_show_fee"),
|
||||
disabled: cardCharged,
|
||||
onClick: () => {
|
||||
setChargeCardDialogIsOpen(true);
|
||||
},
|
||||
icon: FiCreditCard,
|
||||
},
|
||||
];
|
||||
|
||||
if (isTabRecurring && isRecurring) {
|
||||
bookedActions = bookedActions.filter((action) => action.id !== "edit_booking");
|
||||
}
|
||||
@@ -254,6 +282,15 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
isOpenDialog={isOpenSetLocationDialog}
|
||||
setShowLocationModal={setIsOpenLocationDialog}
|
||||
/>
|
||||
{booking.paid && (
|
||||
<ChargeCardDialog
|
||||
isOpenDialog={chargeCardDialogIsOpen}
|
||||
setIsOpenDialog={setChargeCardDialogIsOpen}
|
||||
bookingId={booking.id}
|
||||
paymentAmount={booking?.payment[0].amount}
|
||||
paymentCurrency={booking?.payment[0].currency}
|
||||
/>
|
||||
)}
|
||||
{showRecordingsButtons && (
|
||||
<ViewRecordingsDialog
|
||||
booking={booking}
|
||||
@@ -321,7 +358,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
)}
|
||||
{booking.paid && (
|
||||
<Badge className="ltr:mr-2 rtl:ml-2" variant="green">
|
||||
{t("paid")}
|
||||
{booking.payment[0].paymentOption === "HOLD" ? t("card_held") : t("paid")}
|
||||
</Badge>
|
||||
)}
|
||||
{recurringDates !== undefined && (
|
||||
@@ -423,6 +460,11 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
<RequestSentMessage />
|
||||
</div>
|
||||
)}
|
||||
{booking.status === "ACCEPTED" && booking.paid && booking?.payment[0]?.paymentOption === "HOLD" && (
|
||||
<div className="ml-2">
|
||||
<TableActions actions={chargeCardActions} />
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
</>
|
||||
|
||||
@@ -124,6 +124,16 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
||||
[timeZone]
|
||||
);
|
||||
const paymentAppData = getPaymentAppData(eventType);
|
||||
const paymentAmount = () => {
|
||||
return;
|
||||
<IntlProvider locale="en">
|
||||
<FormattedNumber
|
||||
value={paymentAppData.price / 100.0}
|
||||
style="currency"
|
||||
currency={paymentAppData.currency?.toUpperCase()}
|
||||
/>
|
||||
</IntlProvider>;
|
||||
};
|
||||
const rainbowAppData = getEventTypeAppData(eventType, "rainbow") || {};
|
||||
const rawSlug = profile.slug ? profile.slug.split("/") : [];
|
||||
if (rawSlug.length > 1) rawSlug.pop(); //team events have team name as slug, but user events have [user]/[type] as slug.
|
||||
@@ -237,13 +247,22 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
||||
{paymentAppData.price > 0 && (
|
||||
<p className="-ml-2 px-2 text-sm font-medium">
|
||||
<FiCreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||
<IntlProvider locale="en">
|
||||
<FormattedNumber
|
||||
value={paymentAppData.price / 100.0}
|
||||
style="currency"
|
||||
currency={paymentAppData.currency?.toUpperCase()}
|
||||
/>
|
||||
</IntlProvider>
|
||||
{paymentAppData.paymentOption === "HOLD" ? (
|
||||
<>
|
||||
{t("no_show_fee_amount", {
|
||||
amount: paymentAppData.price / 100.0,
|
||||
formatParams: { amount: { currency: paymentAppData.currency } },
|
||||
})}
|
||||
</>
|
||||
) : (
|
||||
<IntlProvider locale="en">
|
||||
<FormattedNumber
|
||||
value={paymentAppData.price / 100.0}
|
||||
style="currency"
|
||||
currency={paymentAppData.currency?.toUpperCase()}
|
||||
/>
|
||||
</IntlProvider>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
{timezoneDropdown}
|
||||
|
||||
@@ -529,15 +529,15 @@ const BookingPage = ({
|
||||
className={classNames(
|
||||
"main",
|
||||
isBackgroundTransparent ? "" : "dark:bg-darkgray-100 bg-default dark:border",
|
||||
"dark:border-darkgray-300 rounded-md sm:border"
|
||||
"border-subtle rounded-md sm:border"
|
||||
)}>
|
||||
<div className="sm:flex">
|
||||
{showEventTypeDetails && (
|
||||
<div className="sm:dark:border-darkgray-300 text-default flex flex-col px-6 pt-6 pb-0 sm:w-1/2 sm:border-r sm:pb-6">
|
||||
<div className="sm:border-subtle text-default flex flex-col px-6 pt-6 pb-0 sm:w-1/2 sm:border-r sm:pb-6">
|
||||
<BookingDescription isBookingPage profile={profile} eventType={eventType}>
|
||||
<BookingDescriptionPayment eventType={eventType} />
|
||||
<BookingDescriptionPayment eventType={eventType} t={t} />
|
||||
{!rescheduleUid && eventType.recurringEvent?.freq && recurringEventCount && (
|
||||
<div className="dark:text-inverted text-default items-start text-sm font-medium">
|
||||
<div className="text-default items-start text-sm font-medium">
|
||||
<FiRefreshCw className="ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||
<p className="-ml-2 inline-block items-center px-2">
|
||||
{getEveryFreqFor({
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import { Trans } from "next-i18next";
|
||||
import { useState } from "react";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import { IntlProvider, FormattedNumber } from "react-intl";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
showToast,
|
||||
} from "@calcom/ui";
|
||||
import { FiCreditCard, FiAlertTriangle } from "@calcom/ui/components/icon";
|
||||
|
||||
interface IRescheduleDialog {
|
||||
isOpenDialog: boolean;
|
||||
setIsOpenDialog: Dispatch<SetStateAction<boolean>>;
|
||||
bookingId: number;
|
||||
paymentAmount: number;
|
||||
paymentCurrency: string;
|
||||
}
|
||||
|
||||
export const ChargeCardDialog = (props: IRescheduleDialog) => {
|
||||
const { t } = useLocale();
|
||||
const utils = trpc.useContext();
|
||||
const { isOpenDialog, setIsOpenDialog, bookingId } = props;
|
||||
const [chargeError, setChargeError] = useState(false);
|
||||
const chargeCardMutation = trpc.viewer.payments.chargeCard.useMutation({
|
||||
onSuccess: () => {
|
||||
utils.viewer.bookings.invalidate();
|
||||
setIsOpenDialog(false);
|
||||
showToast("Charge successful", "success");
|
||||
},
|
||||
onError: () => {
|
||||
setChargeError(true);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}>
|
||||
<DialogContent>
|
||||
<div className="flex flex-row space-x-3">
|
||||
<div className="flex h-10 w-10 flex-shrink-0 justify-center rounded-full bg-[#FAFAFA]">
|
||||
<FiCreditCard className="m-auto h-6 w-6" />
|
||||
</div>
|
||||
<div className="pt-1">
|
||||
<DialogHeader title={t("charge_card")} />
|
||||
<Trans i18nKey="charge_card_dialog_body">
|
||||
<p className="text-sm text-gray-500">
|
||||
You are about to charge the attendee{" "}
|
||||
<IntlProvider locale="en">
|
||||
<FormattedNumber
|
||||
value={props.paymentAmount / 100.0}
|
||||
style="currency"
|
||||
currency={props.paymentCurrency?.toUpperCase()}
|
||||
/>
|
||||
</IntlProvider>
|
||||
. Are you sure you want to continue?
|
||||
</p>
|
||||
</Trans>
|
||||
|
||||
{chargeError && (
|
||||
<div className="mt-4 flex text-red-500">
|
||||
<FiAlertTriangle className="mr-2 h-5 w-5 " aria-hidden="true" />
|
||||
<p className="text-sm">{t("error_charging_card")}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<DialogClose />
|
||||
<Button
|
||||
data-testid="send_request"
|
||||
disabled={chargeCardMutation.isLoading || chargeError}
|
||||
onClick={() =>
|
||||
chargeCardMutation.mutate({
|
||||
bookingId,
|
||||
})
|
||||
}>
|
||||
<Trans i18nKey="charge_card_confirm">
|
||||
Charge attendee{" "}
|
||||
<IntlProvider locale="en">
|
||||
<FormattedNumber
|
||||
value={props.paymentAmount / 100.0}
|
||||
style="currency"
|
||||
currency={props.paymentCurrency?.toUpperCase()}
|
||||
/>
|
||||
</IntlProvider>
|
||||
</Trans>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -16,7 +16,7 @@ import { APP_NAME, CAL_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button, Checkbox, Label, SettingsToggle, showToast, TextField, Tooltip } from "@calcom/ui";
|
||||
import { Button, Checkbox, Label, SettingsToggle, showToast, TextField, Tooltip, Alert } from "@calcom/ui";
|
||||
import { FiEdit, FiCopy } from "@calcom/ui/components/icon";
|
||||
|
||||
import RequiresConfirmationController from "./RequiresConfirmationController";
|
||||
@@ -58,6 +58,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
const [requiresConfirmation, setRequiresConfirmation] = useState(eventType.requiresConfirmation);
|
||||
const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${eventType.slug}`;
|
||||
const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled");
|
||||
const noShowFeeEnabled = eventType.metadata.apps?.stripe?.paymentOption === "HOLD";
|
||||
|
||||
useEffect(() => {
|
||||
!hashedUrl && setHashedUrl(generateHashedLink(eventType.users[0]?.id ?? team?.id));
|
||||
@@ -262,54 +263,58 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
control={formMethods.control}
|
||||
defaultValue={!!eventType.seatsPerTimeSlot}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<SettingsToggle
|
||||
data-testid="offer-seats-toggle"
|
||||
title={t("offer_seats")}
|
||||
description={t("offer_seats_description")}
|
||||
checked={value}
|
||||
onCheckedChange={(e) => {
|
||||
// Enabling seats will disable guests and requiring confirmation until fully supported
|
||||
if (e) {
|
||||
toggleGuests(false);
|
||||
formMethods.setValue("requiresConfirmation", false);
|
||||
setRequiresConfirmation(false);
|
||||
formMethods.setValue("seatsPerTimeSlot", 2);
|
||||
} else {
|
||||
formMethods.setValue("seatsPerTimeSlot", null);
|
||||
toggleGuests(true);
|
||||
}
|
||||
onChange(e);
|
||||
}}>
|
||||
<Controller
|
||||
name="seatsPerTimeSlot"
|
||||
control={formMethods.control}
|
||||
defaultValue={eventType.seatsPerTimeSlot}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<div className="lg:-ml-2">
|
||||
<TextField
|
||||
required
|
||||
name="seatsPerTimeSlot"
|
||||
labelSrOnly
|
||||
label={t("number_of_seats")}
|
||||
type="number"
|
||||
defaultValue={value || 2}
|
||||
min={1}
|
||||
addOnSuffix={<>{t("seats")}</>}
|
||||
onChange={(e) => {
|
||||
onChange(Math.abs(Number(e.target.value)));
|
||||
}}
|
||||
/>
|
||||
<div className="mt-2">
|
||||
<Checkbox
|
||||
description={t("show_attendees")}
|
||||
onChange={(e) => formMethods.setValue("seatsShowAttendees", e.target.checked)}
|
||||
defaultChecked={!!eventType.seatsShowAttendees}
|
||||
<>
|
||||
<SettingsToggle
|
||||
data-testid="offer-seats-toggle"
|
||||
title={t("offer_seats")}
|
||||
description={t("offer_seats_description")}
|
||||
checked={value}
|
||||
disabled={noShowFeeEnabled}
|
||||
onCheckedChange={(e) => {
|
||||
// Enabling seats will disable guests and requiring confirmation until fully supported
|
||||
if (e) {
|
||||
toggleGuests(false);
|
||||
formMethods.setValue("requiresConfirmation", false);
|
||||
setRequiresConfirmation(false);
|
||||
formMethods.setValue("seatsPerTimeSlot", 2);
|
||||
} else {
|
||||
formMethods.setValue("seatsPerTimeSlot", null);
|
||||
toggleGuests(true);
|
||||
}
|
||||
onChange(e);
|
||||
}}>
|
||||
<Controller
|
||||
name="seatsPerTimeSlot"
|
||||
control={formMethods.control}
|
||||
defaultValue={eventType.seatsPerTimeSlot}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<div className="lg:-ml-2">
|
||||
<TextField
|
||||
required
|
||||
name="seatsPerTimeSlot"
|
||||
labelSrOnly
|
||||
label={t("number_of_seats")}
|
||||
type="number"
|
||||
defaultValue={value || 2}
|
||||
min={1}
|
||||
addOnSuffix={<>{t("seats")}</>}
|
||||
onChange={(e) => {
|
||||
onChange(Math.abs(Number(e.target.value)));
|
||||
}}
|
||||
/>
|
||||
<div className="mt-2">
|
||||
<Checkbox
|
||||
description={t("show_attendees")}
|
||||
onChange={(e) => formMethods.setValue("seatsShowAttendees", e.target.checked)}
|
||||
defaultChecked={!!eventType.seatsShowAttendees}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</SettingsToggle>
|
||||
)}
|
||||
/>
|
||||
</SettingsToggle>
|
||||
{noShowFeeEnabled && <Alert severity="warning" title={t("seats_and_no_show_fee_error")} />}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
|
||||
|
||||
@@ -84,14 +84,14 @@ const CalcomThemeProvider = (
|
||||
// One such example is our Embeds Demo and Testing page at http://localhost:3100
|
||||
// Having `getEmbedNamespace` defined on window before react initializes the app, ensures that embedNamespace is available on the first mount and can be used as part of storageKey
|
||||
const embedNamespace = typeof window !== "undefined" ? window.getEmbedNamespace() : null;
|
||||
const isEmbedMode = typeof embedNamespace === "string";
|
||||
// If embedNamespace is not defined, we use the default storageKey -> The default storage key changs based on if we force light mode or not
|
||||
// This is done to ensure that the default theme is light when we force light mode and as soon as you navigate to a page that is dark we dont need a hard refresh to change
|
||||
const storageKey =
|
||||
typeof embedNamespace === "string"
|
||||
? `embed-theme-${embedNamespace}`
|
||||
: !isThemeSupported
|
||||
? "cal-light"
|
||||
: "theme";
|
||||
const storageKey = isEmbedMode
|
||||
? `embed-theme-${embedNamespace}`
|
||||
: !isThemeSupported
|
||||
? "cal-light"
|
||||
: "theme";
|
||||
|
||||
return (
|
||||
<ThemeProvider
|
||||
@@ -101,6 +101,17 @@ const CalcomThemeProvider = (
|
||||
forcedTheme={forcedTheme}
|
||||
storageKey={storageKey}
|
||||
attribute="class">
|
||||
{/* Embed Mode can be detected reliably only on client side here as there can be static generated pages as well which can't determine if it's embed mode at backend */}
|
||||
{/* color-scheme makes background:transparent not work in iframe which is required by embed. */}
|
||||
{typeof window !== "undefined" && !isEmbedMode && (
|
||||
<style jsx global>
|
||||
{`
|
||||
.dark {
|
||||
color-scheme: dark;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
)}
|
||||
{props.children}
|
||||
</ThemeProvider>
|
||||
);
|
||||
@@ -121,7 +132,6 @@ const AppProviders = (props: AppPropsWithChildren) => {
|
||||
<SessionProvider session={session || undefined}>
|
||||
<CustomI18nextProvider {...props}>
|
||||
<TooltipProvider>
|
||||
{/* color-scheme makes background:transparent not work which is required by embed. We need to ensure next-theme adds color-scheme to `body` instead of `html`(https://github.com/pacocoursey/next-themes/blob/main/src/index.tsx#L74). Once that's done we can enable color-scheme support */}
|
||||
<CalcomThemeProvider
|
||||
nonce={props.pageProps.nonce}
|
||||
isThemeSupported={props.Component.isThemeSupported}>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "2.7.12",
|
||||
"version": "2.7.15",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
|
||||
@@ -126,41 +126,6 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
const { eventType, locationOptions, team, teamMembers, currentUserMembership, destinationCalendar } = props;
|
||||
const [animationParentRef] = useAutoAnimate<HTMLDivElement>();
|
||||
|
||||
const updateMutation = trpc.viewer.eventTypes.update.useMutation({
|
||||
onSuccess: async () => {
|
||||
showToast(
|
||||
t("event_type_updated_successfully", {
|
||||
eventTypeTitle: eventType.title,
|
||||
}),
|
||||
"success"
|
||||
);
|
||||
},
|
||||
async onSettled() {
|
||||
await utils.viewer.eventTypes.get.invalidate();
|
||||
},
|
||||
onError: (err) => {
|
||||
let message = "";
|
||||
if (err instanceof HttpError) {
|
||||
const message = `${err.statusCode}: ${err.message}`;
|
||||
showToast(message, "error");
|
||||
}
|
||||
|
||||
if (err.data?.code === "UNAUTHORIZED") {
|
||||
message = `${err.data.code}: You are not able to update this event`;
|
||||
}
|
||||
|
||||
if (err.data?.code === "PARSE_ERROR" || err.data?.code === "BAD_REQUEST") {
|
||||
message = `${err.data.code}: ${err.message}`;
|
||||
}
|
||||
|
||||
if (message) {
|
||||
showToast(message, "error");
|
||||
} else {
|
||||
showToast(err.message, "error");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const [periodDates] = useState<{ startDate: Date; endDate: Date }>({
|
||||
startDate: new Date(eventType.periodStartDate || Date.now()),
|
||||
endDate: new Date(eventType.periodEndDate || Date.now()),
|
||||
@@ -235,6 +200,48 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
),
|
||||
});
|
||||
|
||||
const updateMutation = trpc.viewer.eventTypes.update.useMutation({
|
||||
onSuccess: async () => {
|
||||
showToast(
|
||||
t("event_type_updated_successfully", {
|
||||
eventTypeTitle: eventType.title,
|
||||
}),
|
||||
"success"
|
||||
);
|
||||
},
|
||||
async onSettled() {
|
||||
await utils.viewer.eventTypes.get.invalidate();
|
||||
},
|
||||
onError: (err) => {
|
||||
let message = "";
|
||||
if (err instanceof HttpError) {
|
||||
const message = `${err.statusCode}: ${err.message}`;
|
||||
showToast(message, "error");
|
||||
}
|
||||
|
||||
if (err.data?.code === "UNAUTHORIZED") {
|
||||
message = `${err.data.code}: You are not able to update this event`;
|
||||
}
|
||||
|
||||
if (err.data?.code === "PARSE_ERROR" || err.data?.code === "BAD_REQUEST") {
|
||||
message = `${err.data.code}: ${err.message}`;
|
||||
}
|
||||
|
||||
if (message) {
|
||||
showToast(message, "error");
|
||||
} else {
|
||||
showToast(err.message, "error");
|
||||
}
|
||||
|
||||
console.log(formMethods.formState.isSubmitting);
|
||||
|
||||
// formMethods.setFormState((prevState) => ({
|
||||
// ...prevState,
|
||||
// isSubmitting: false,
|
||||
// }));
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!formMethods.formState.isDirty) {
|
||||
//TODO: What's the best way to sync the form with backend
|
||||
@@ -337,6 +344,10 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (metadata?.apps?.stripe?.paymentOption === "HOLD" && seatsPerTimeSlot) {
|
||||
throw new Error(t("seats_and_no_show_fee_error"));
|
||||
}
|
||||
|
||||
updateMutation.mutate({
|
||||
...input,
|
||||
locations,
|
||||
|
||||
@@ -55,12 +55,12 @@ function TeamPage({ team, isUnpublished }: TeamPageProps) {
|
||||
}
|
||||
|
||||
const EventTypes = () => (
|
||||
<ul className="dark:border-darkgray-300 border-subtle rounded-md border">
|
||||
<ul className="border-subtle border-subtle rounded-md border">
|
||||
{team.eventTypes.map((type, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className={classNames(
|
||||
"dark:bg-darkgray-100 dark:border-darkgray-300 bg-default hover:bg-muted border-subtle group relative border-b first:rounded-t-md last:rounded-b-md last:border-b-0",
|
||||
"dark:bg-darkgray-100 border-subtle bg-default hover:bg-muted border-subtle group relative border-b first:rounded-t-md last:rounded-b-md last:border-b-0",
|
||||
!isEmbed && "bg-default"
|
||||
)}>
|
||||
<div className="px-6 py-4 ">
|
||||
@@ -125,7 +125,7 @@ function TeamPage({ team, isUnpublished }: TeamPageProps) {
|
||||
<div>
|
||||
<div className="relative mt-12">
|
||||
<div className="absolute inset-0 flex items-center" aria-hidden="true">
|
||||
<div className="dark:border-darkgray-300 border-subtle w-full border-t" />
|
||||
<div className="border-subtle border-subtle w-full border-t" />
|
||||
</div>
|
||||
<div className="relative flex justify-center">
|
||||
<span className="dark:bg-darkgray-50 bg-subtle text-subtle dark:text-inverted px-2 text-sm">
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||
|
||||
import { TeamsListing } from "@calcom/features/ee/teams/components";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
@@ -25,6 +27,14 @@ function Teams() {
|
||||
);
|
||||
}
|
||||
|
||||
export const getStaticProps = async () => {
|
||||
return {
|
||||
props: {
|
||||
...(await serverSideTranslations("en", ["common"])),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
Teams.requiresLicense = false;
|
||||
|
||||
export default Teams;
|
||||
|
||||
@@ -1693,7 +1693,29 @@
|
||||
"redirect_url_warning": "Adding a redirect will disable the success page. Make sure to mention \"Booking Confirmed\" on your custom success page.",
|
||||
"event_trends": "Event Trends",
|
||||
"clear_filters": "Clear Filters",
|
||||
"hold": "Hold",
|
||||
"on_booking_option": "Collect payment on booking",
|
||||
"hold_option": "Charge no-show fee",
|
||||
"card_held": "Card held",
|
||||
"charge_card": "Charge card",
|
||||
"card_charged": "Card charged",
|
||||
"no_show_fee_amount": "{{amount, currency}} no-show fee",
|
||||
"no_show_fee": "No-show Fee",
|
||||
"submit_card": "Submit card",
|
||||
"submit_payment_information": "Submit payment information",
|
||||
"meeting_awaiting_payment_method": "Your meeting is awaiting a payment method",
|
||||
"no_show_fee_charged_email_subject": "No-show fee of {{amount, currency}} charged for {{title}} at {{date}}",
|
||||
"no_show_fee_charged_text_body": "No-show fee was charged",
|
||||
"no_show_fee_charged_subtitle": "No-show fee of {{amount, currency}} was charged for the following event",
|
||||
"error_charging_card": "Something went wrong charging the no-show fee. Please try again later.",
|
||||
"collect_no_show_fee": "Collect no-show fee",
|
||||
"no_show_fee_charged": "No-show fee charged",
|
||||
"insights": "Insights",
|
||||
"insights_no_data_found_for_filter": "No data found for the selected filter or selected dates.",
|
||||
"acknowledge_booking_no_show_fee": "I acknowledge that if I do not attend this event that a {{amount, currency}} no show fee will be applied to my card.",
|
||||
"card_details": "Card details",
|
||||
"seats_and_no_show_fee_error": "Currently cannot enable seats and charge a no-show fee",
|
||||
"complete_your_booking": "Complete your booking",
|
||||
"complete_your_booking_subject": "Complete your booking: {{title}} on {{date}}",
|
||||
"email_invite_team": "{{email}} has been invited"
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
"calcom_explained_new_user": "Terminez la configuration de votre compte {{appName}} ! Vous n'êtes qu'à quelques pas de résoudre tous vos problèmes de planification.",
|
||||
"have_any_questions": "Vous avez des questions ? Nous sommes là pour vous aider.",
|
||||
"reset_password_subject": "{{appName}} : Instructions de réinitialisation de mot de passe",
|
||||
"event_declined_subject": "Refusé : {{title}} à {{date}}",
|
||||
"event_cancelled_subject": "Annulé : {{title}} à {{date}}",
|
||||
"event_declined_subject": "Refusé : {{title}} le {{date}}",
|
||||
"event_cancelled_subject": "Annulé : {{title}} le {{date}}",
|
||||
"event_request_declined": "Votre demande d'événement a été refusée",
|
||||
"event_request_declined_recurring": "Votre demande d'événement récurrent a été refusée",
|
||||
"event_request_cancelled": "Votre événement planifié a été annulé",
|
||||
@@ -66,11 +66,11 @@
|
||||
"someone_requested_an_event": "Quelqu'un a demandé à planifier un événement sur votre calendrier.",
|
||||
"someone_requested_password_reset": "Quelqu'un a demandé un lien pour modifier votre mot de passe.",
|
||||
"password_reset_instructions": "Si vous n'en avez pas fait la demande, vous pouvez ignorer cet e-mail en toute sécurité et votre mot de passe ne sera pas modifié.",
|
||||
"event_awaiting_approval_subject": "En attente de validation : {{title}} à {{date}}",
|
||||
"event_awaiting_approval_subject": "En attente de validation : {{title}} le {{date}}",
|
||||
"event_still_awaiting_approval": "Un événement est toujours en attente de votre validation",
|
||||
"booking_submitted_subject": "Réservation confirmée : {{eventType}} avec {{name}} le {{date}}",
|
||||
"booking_submitted_subject": "Réservation confirmée : {{title}} le {{date}}",
|
||||
"your_meeting_has_been_booked": "Votre rendez-vous a été réservé",
|
||||
"event_type_has_been_rescheduled_on_time_date": "Votre {{title}} a été replanifié à {{date}}",
|
||||
"event_type_has_been_rescheduled_on_time_date": "Votre {{title}} a été replanifié le {{date}}",
|
||||
"event_has_been_rescheduled": "Mise à jour - Votre événement a été replanifié",
|
||||
"request_reschedule_title_attendee": "Demander à replanifier votre réservation",
|
||||
"request_reschedule_subtitle": "{{organizer}} a annulé la réservation et vous a demandé de choisir un autre moment.",
|
||||
@@ -944,7 +944,7 @@
|
||||
"impersonating_user_warning": "Emprunt du nom d'utilisateur « {{user}} ».",
|
||||
"impersonating_stop_instructions": "Cliquez ici pour arrêter",
|
||||
"event_location_changed": "Mise à jour - Votre événement a changé de lieu",
|
||||
"location_changed_event_type_subject": "Lieu modifié : {{eventType}} avec {{name}} à {{date}}",
|
||||
"location_changed_event_type_subject": "Lieu modifié : {{eventType}} avec {{name}} le {{date}}",
|
||||
"current_location": "Lieu actuel",
|
||||
"user_phone": "Votre numéro de téléphone",
|
||||
"new_location": "Nouveau lieu",
|
||||
@@ -1120,8 +1120,8 @@
|
||||
"close": "Fermer",
|
||||
"upgrade": "Mettre à niveau",
|
||||
"upgrade_to_access_recordings_title": "Mettez à niveau pour accéder aux enregistrements",
|
||||
"upgrade_to_access_recordings_description": "Les enregistrements ne sont disponibles que dans le plan d'équipes. Mettez à niveau pour commencer à enregistrer vos appels.",
|
||||
"recordings_are_part_of_the_teams_plan": "Les enregistrements font partie du plan d'équipes",
|
||||
"upgrade_to_access_recordings_description": "Les enregistrements ne sont disponibles que dans le plan Équipes. Mettez à niveau pour commencer à enregistrer vos appels.",
|
||||
"recordings_are_part_of_the_teams_plan": "Les enregistrements font partie du plan Équipes",
|
||||
"team_feature_teams": "Ceci est une fonctionnalité d'équipe. Mettez à jour vers le plan Équipes pour voir les disponibilités de votre équipe.",
|
||||
"team_feature_workflows": "Ceci est une fonctionnalité d'équipe. Mettez à jour vers le plan Équipes pour automatiser vos notifications et rappels d'événements avec les workflows.",
|
||||
"show_eventtype_on_profile": "Afficher sur le profil",
|
||||
@@ -1649,13 +1649,13 @@
|
||||
"add_1_option_per_line": "Ajouter une option par ligne",
|
||||
"select_a_router": "Sélectionner un routeur",
|
||||
"add_a_new_route": "Ajouter une nouvelle route",
|
||||
"make_informed_decisions": "Prenez des décisions éclairées avec Insights",
|
||||
"make_informed_decisions_description": "Notre tableau de bord Insights analyse toute l'activité de votre équipe et vous révèle les tendances qui permettent d'améliorer la planification et la prise de décision au sein de l'équipe.",
|
||||
"view_bookings_across": "Voir les réservations de tous les membres",
|
||||
"make_informed_decisions": "Prenez des décisions éclairées grâce aux statistiques",
|
||||
"make_informed_decisions_description": "Notre tableau de bord de statistiques analyse toute l'activité de votre équipe et vous révèle les tendances qui permettent d'améliorer la planification et la prise de décision au sein de l'équipe.",
|
||||
"view_bookings_across": "Visualisez les réservations de tous les membres",
|
||||
"view_bookings_across_description": "Découvrez qui reçoit le plus de réservations et assurez la meilleure répartition au sein de votre équipe.",
|
||||
"identify_booking_trends": "Identifier les tendances de réservation",
|
||||
"identify_booking_trends": "Identifiez les tendances de réservation",
|
||||
"identify_booking_trends_description": "Découvrez quels jours de la semaine et quelles heures de la journée sont populaires auprès de vos utilisateurs.",
|
||||
"spot_popular_event_types": "Identifier les types d'événements populaires",
|
||||
"spot_popular_event_types": "Identifiez les types d'événements populaires",
|
||||
"spot_popular_event_types_description": "Découvrez lesquels de vos types d'événements reçoivent le plus de clics et de réservations.",
|
||||
"no_responses_yet": "Aucune réponse pour l'instant",
|
||||
"this_will_be_the_placeholder": "Ce sera le texte de substitution",
|
||||
@@ -1688,12 +1688,34 @@
|
||||
"events_rescheduled": "Événements replanifiés",
|
||||
"from_last_period": "depuis la dernière période",
|
||||
"from_to_date_period": "De : {{startDate}} À : {{endDate}}",
|
||||
"analytics_for_organisation": "Insights",
|
||||
"analytics_for_organisation": "Statistiques",
|
||||
"subtitle_analytics": "En savoir plus sur l'activité de votre équipe",
|
||||
"redirect_url_warning": "L'ajout d'une redirection désactivera la page de succès. Assurez-vous de mentionner « Réservation confirmée » sur votre page de succès personnalisée.",
|
||||
"event_trends": "Tendances de l'événement",
|
||||
"clear_filters": "Effacer les filtres",
|
||||
"insights": "Insights",
|
||||
"hold": "Retenir",
|
||||
"on_booking_option": "Percevoir le paiement à la réservation",
|
||||
"hold_option": "Facturer des frais d'absence",
|
||||
"card_held": "Carte retenue",
|
||||
"charge_card": "Débiter la carte",
|
||||
"card_charged": "Carte débitée",
|
||||
"no_show_fee_amount": "Frais d'absence de {{amount, currency}}",
|
||||
"no_show_fee": "Frais d'absence",
|
||||
"submit_card": "Soumettre la carte",
|
||||
"submit_payment_information": "Soumettre les informations de paiement",
|
||||
"meeting_awaiting_payment_method": "Votre rendez-vous est en attente d'un moyen de paiement",
|
||||
"no_show_fee_charged_email_subject": "Frais d'absence de {{amount, currency}} facturés pour {{title}} le {{date}}",
|
||||
"no_show_fee_charged_text_body": "Des frais d'absence ont été facturés",
|
||||
"no_show_fee_charged_subtitle": "Des frais d'absence de {{amount, currency}} ont été facturés pour l'événement suivant",
|
||||
"error_charging_card": "Une erreur s'est produite lors de la facturation des frais d'absence. Veuillez réessayer plus tard.",
|
||||
"collect_no_show_fee": "Percevoir les frais d'absence",
|
||||
"no_show_fee_charged": "Frais d'absence facturés",
|
||||
"insights": "Statistiques",
|
||||
"insights_no_data_found_for_filter": "Aucune donnée trouvée pour le filtre ou les dates sélectionnés.",
|
||||
"acknowledge_booking_no_show_fee": "Je reconnais que si je ne participe pas à cet événement, des frais d'absence de {{amount, currency}} seront appliqués à ma carte.",
|
||||
"card_details": "Détails de la carte",
|
||||
"seats_and_no_show_fee_error": "Il n'est pas possible d'activer les places et de facturer des frais d'absence pour le moment",
|
||||
"complete_your_booking": "Terminer votre réservation",
|
||||
"complete_your_booking_subject": "Terminer votre réservation : {{title}} le {{date}}",
|
||||
"email_invite_team": "{{email}} a été invité"
|
||||
}
|
||||
|
||||
@@ -44,8 +44,6 @@
|
||||
--cal-brand-text: white;
|
||||
}
|
||||
.dark {
|
||||
color-scheme: dark;
|
||||
|
||||
/* background */
|
||||
|
||||
--cal-bg-emphasis: #2b2b2b;
|
||||
|
||||
@@ -112,6 +112,7 @@ export const BaseAppFork = {
|
||||
isTemplate,
|
||||
// Store the template used to create an app
|
||||
__template: template,
|
||||
dirName: slug,
|
||||
};
|
||||
const currentConfig = JSON.parse(fs.readFileSync(`${appDirPath}/config.json`).toString());
|
||||
config = {
|
||||
|
||||
@@ -28,6 +28,7 @@ export const getEventTypeAppData = <T extends EventTypeAppsList>(
|
||||
price: eventType.price,
|
||||
// Currency default is "usd" in DB.So, it would also be available always
|
||||
currency: eventType.currency,
|
||||
paymentOption: "ON_BOOKING",
|
||||
},
|
||||
rainbow: {
|
||||
enabled: !!(eventType.metadata?.smartContractAddress && eventType.metadata?.blockchainId),
|
||||
|
||||
@@ -73,7 +73,7 @@ function useAddAppMutation(_type: App["type"] | null, allOptions?: UseAddAppMuta
|
||||
|
||||
if (!isOmniInstall) {
|
||||
gotoUrl(json.url, json.newTab);
|
||||
return;
|
||||
return { setupPending: externalUrl || json.url.endsWith("/setup") };
|
||||
}
|
||||
|
||||
// Skip redirection only if it is an OmniInstall and redirect URL isn't of some other origin
|
||||
@@ -83,7 +83,7 @@ function useAddAppMutation(_type: App["type"] | null, allOptions?: UseAddAppMuta
|
||||
if (externalUrl) {
|
||||
// TODO: For Omni installation to authenticate and come back to the page where installation was initiated, some changes need to be done in all apps' add callbacks
|
||||
gotoUrl(json.url, json.newTab);
|
||||
return;
|
||||
return { setupPending: externalUrl };
|
||||
}
|
||||
|
||||
return { setupPending: externalUrl || json.url.endsWith("/setup") };
|
||||
|
||||
@@ -12,5 +12,6 @@
|
||||
"email": "support@cal.com",
|
||||
"description": "The joyful productivity app\r\r",
|
||||
"__createdUsingCli": true,
|
||||
"dependencies": ["google-calendar"]
|
||||
"dependencies": ["google-calendar"],
|
||||
"dirName": "amie"
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import type { InstallAppButtonProps } from "@calcom/app-store/types";
|
||||
|
||||
import useAddAppMutation from "../../_utils/useAddAppMutation";
|
||||
|
||||
export default function InstallAppButton(props: InstallAppButtonProps) {
|
||||
const mutation = useAddAppMutation("apple_calendar");
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.render({
|
||||
onClick() {
|
||||
mutation.mutate("");
|
||||
},
|
||||
loading: mutation.isLoading,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { default as InstallAppButton } from "./InstallAppButton";
|
||||
@@ -5,27 +5,10 @@
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
export const InstallAppButtonMap = {
|
||||
applecalendar: dynamic(() => import("./applecalendar/components/InstallAppButton")),
|
||||
around: dynamic(() => import("./around/components/InstallAppButton")),
|
||||
caldavcalendar: dynamic(() => import("./caldavcalendar/components/InstallAppButton")),
|
||||
closecom: dynamic(() => import("./closecom/components/InstallAppButton")),
|
||||
exchange2013calendar: dynamic(() => import("./exchange2013calendar/components/InstallAppButton")),
|
||||
exchange2016calendar: dynamic(() => import("./exchange2016calendar/components/InstallAppButton")),
|
||||
exchangecalendar: dynamic(() => import("./exchangecalendar/components/InstallAppButton")),
|
||||
googlecalendar: dynamic(() => import("./googlecalendar/components/InstallAppButton")),
|
||||
hubspot: dynamic(() => import("./hubspot/components/InstallAppButton")),
|
||||
huddle01video: dynamic(() => import("./huddle01video/components/InstallAppButton")),
|
||||
jitsivideo: dynamic(() => import("./jitsivideo/components/InstallAppButton")),
|
||||
larkcalendar: dynamic(() => import("./larkcalendar/components/InstallAppButton")),
|
||||
office365calendar: dynamic(() => import("./office365calendar/components/InstallAppButton")),
|
||||
office365video: dynamic(() => import("./office365video/components/InstallAppButton")),
|
||||
riverside: dynamic(() => import("./riverside/components/InstallAppButton")),
|
||||
tandemvideo: dynamic(() => import("./tandemvideo/components/InstallAppButton")),
|
||||
vital: dynamic(() => import("./vital/components/InstallAppButton")),
|
||||
whereby: dynamic(() => import("./whereby/components/InstallAppButton")),
|
||||
wipemycalother: dynamic(() => import("./wipemycalother/components/InstallAppButton")),
|
||||
zapier: dynamic(() => import("./zapier/components/InstallAppButton")),
|
||||
zoomvideo: dynamic(() => import("./zoomvideo/components/InstallAppButton")),
|
||||
};
|
||||
export const AppSettingsComponentsMap = {
|
||||
"general-app-settings": dynamic(() =>
|
||||
@@ -40,7 +23,7 @@ export const EventTypeAddonMap = {
|
||||
fathom: dynamic(() => import("./fathom/components/EventTypeAppCardInterface")),
|
||||
ga4: dynamic(() => import("./ga4/components/EventTypeAppCardInterface")),
|
||||
giphy: dynamic(() => import("./giphy/components/EventTypeAppCardInterface")),
|
||||
"google-tag-manager": dynamic(() => import("./google-tag-manager/components/EventTypeAppCardInterface")),
|
||||
gtm: dynamic(() => import("./gtm/components/EventTypeAppCardInterface")),
|
||||
plausible: dynamic(() => import("./plausible/components/EventTypeAppCardInterface")),
|
||||
qr_code: dynamic(() => import("./qr_code/components/EventTypeAppCardInterface")),
|
||||
rainbow: dynamic(() => import("./rainbow/components/EventTypeAppCardInterface")),
|
||||
|
||||
@@ -6,8 +6,8 @@ import { appKeysSchema as dailyvideo_zod_ts } from "./dailyvideo/zod";
|
||||
import { appKeysSchema as fathom_zod_ts } from "./fathom/zod";
|
||||
import { appKeysSchema as ga4_zod_ts } from "./ga4/zod";
|
||||
import { appKeysSchema as giphy_zod_ts } from "./giphy/zod";
|
||||
import { appKeysSchema as google_tag_manager_zod_ts } from "./google-tag-manager/zod";
|
||||
import { appKeysSchema as googlecalendar_zod_ts } from "./googlecalendar/zod";
|
||||
import { appKeysSchema as gtm_zod_ts } from "./gtm/zod";
|
||||
import { appKeysSchema as hubspot_zod_ts } from "./hubspot/zod";
|
||||
import { appKeysSchema as larkcalendar_zod_ts } from "./larkcalendar/zod";
|
||||
import { appKeysSchema as office365calendar_zod_ts } from "./office365calendar/zod";
|
||||
@@ -31,8 +31,8 @@ export const appKeysSchemas = {
|
||||
fathom: fathom_zod_ts,
|
||||
ga4: ga4_zod_ts,
|
||||
giphy: giphy_zod_ts,
|
||||
"google-tag-manager": google_tag_manager_zod_ts,
|
||||
googlecalendar: googlecalendar_zod_ts,
|
||||
gtm: gtm_zod_ts,
|
||||
hubspot: hubspot_zod_ts,
|
||||
larkcalendar: larkcalendar_zod_ts,
|
||||
office365calendar: office365calendar_zod_ts,
|
||||
|
||||
@@ -18,9 +18,9 @@ import facetime_config_json from "./facetime/config.json";
|
||||
import fathom_config_json from "./fathom/config.json";
|
||||
import ga4_config_json from "./ga4/config.json";
|
||||
import { metadata as giphy__metadata_ts } from "./giphy/_metadata";
|
||||
import google_tag_manager_config_json from "./google-tag-manager/config.json";
|
||||
import { metadata as googlecalendar__metadata_ts } from "./googlecalendar/_metadata";
|
||||
import { metadata as googlevideo__metadata_ts } from "./googlevideo/_metadata";
|
||||
import gtm_config_json from "./gtm/config.json";
|
||||
import { metadata as hubspot__metadata_ts } from "./hubspot/_metadata";
|
||||
import { metadata as huddle01video__metadata_ts } from "./huddle01video/_metadata";
|
||||
import { metadata as jitsivideo__metadata_ts } from "./jitsivideo/_metadata";
|
||||
@@ -79,9 +79,9 @@ export const appStoreMetadata = {
|
||||
fathom: fathom_config_json,
|
||||
ga4: ga4_config_json,
|
||||
giphy: giphy__metadata_ts,
|
||||
"google-tag-manager": google_tag_manager_config_json,
|
||||
googlecalendar: googlecalendar__metadata_ts,
|
||||
googlevideo: googlevideo__metadata_ts,
|
||||
gtm: gtm_config_json,
|
||||
hubspot: hubspot__metadata_ts,
|
||||
huddle01video: huddle01video__metadata_ts,
|
||||
jitsivideo: jitsivideo__metadata_ts,
|
||||
|
||||
@@ -6,8 +6,8 @@ import { appDataSchema as dailyvideo_zod_ts } from "./dailyvideo/zod";
|
||||
import { appDataSchema as fathom_zod_ts } from "./fathom/zod";
|
||||
import { appDataSchema as ga4_zod_ts } from "./ga4/zod";
|
||||
import { appDataSchema as giphy_zod_ts } from "./giphy/zod";
|
||||
import { appDataSchema as google_tag_manager_zod_ts } from "./google-tag-manager/zod";
|
||||
import { appDataSchema as googlecalendar_zod_ts } from "./googlecalendar/zod";
|
||||
import { appDataSchema as gtm_zod_ts } from "./gtm/zod";
|
||||
import { appDataSchema as hubspot_zod_ts } from "./hubspot/zod";
|
||||
import { appDataSchema as larkcalendar_zod_ts } from "./larkcalendar/zod";
|
||||
import { appDataSchema as office365calendar_zod_ts } from "./office365calendar/zod";
|
||||
@@ -31,8 +31,8 @@ export const appDataSchemas = {
|
||||
fathom: fathom_zod_ts,
|
||||
ga4: ga4_zod_ts,
|
||||
giphy: giphy_zod_ts,
|
||||
"google-tag-manager": google_tag_manager_zod_ts,
|
||||
googlecalendar: googlecalendar_zod_ts,
|
||||
gtm: gtm_zod_ts,
|
||||
hubspot: hubspot_zod_ts,
|
||||
larkcalendar: larkcalendar_zod_ts,
|
||||
office365calendar: office365calendar_zod_ts,
|
||||
|
||||
@@ -18,9 +18,9 @@ export const apiHandlers = {
|
||||
fathom: import("./fathom/api"),
|
||||
ga4: import("./ga4/api"),
|
||||
giphy: import("./giphy/api"),
|
||||
"google-tag-manager": import("./google-tag-manager/api"),
|
||||
googlecalendar: import("./googlecalendar/api"),
|
||||
googlevideo: import("./googlevideo/api"),
|
||||
gtm: import("./gtm/api"),
|
||||
hubspot: import("./hubspot/api"),
|
||||
huddle01video: import("./huddle01video/api"),
|
||||
jitsivideo: import("./jitsivideo/api"),
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import type { InstallAppButtonProps } from "@calcom/app-store/types";
|
||||
|
||||
import useAddAppMutation from "../../_utils/useAddAppMutation";
|
||||
|
||||
export default function InstallAppButton(props: InstallAppButtonProps) {
|
||||
const mutation = useAddAppMutation("around_video");
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.render({
|
||||
onClick() {
|
||||
mutation.mutate("");
|
||||
},
|
||||
loading: mutation.isLoading,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { default as InstallAppButton } from "./InstallAppButton";
|
||||
@@ -21,7 +21,6 @@ export const metadata = {
|
||||
url: "https://cal.com/",
|
||||
verified: true,
|
||||
email: "ali@cal.com",
|
||||
dirName: "caldavcalendar",
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import type { InstallAppButtonProps } from "@calcom/app-store/types";
|
||||
|
||||
import useAddAppMutation from "../../_utils/useAddAppMutation";
|
||||
|
||||
export default function InstallAppButton(props: InstallAppButtonProps) {
|
||||
const mutation = useAddAppMutation("caldav_calendar");
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.render({
|
||||
onClick() {
|
||||
mutation.mutate("");
|
||||
},
|
||||
loading: mutation.isLoading,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { default as InstallAppButton } from "./InstallAppButton";
|
||||
@@ -21,6 +21,7 @@ export const metadata = {
|
||||
url: "https://cal.com/",
|
||||
verified: true,
|
||||
email: "help@cal.com",
|
||||
dirName: "caldavcalendar",
|
||||
} as App;
|
||||
|
||||
export * as api from "./api";
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import type { InstallAppButtonProps } from "@calcom/app-store/types";
|
||||
|
||||
import useAddAppMutation from "../../_utils/useAddAppMutation";
|
||||
|
||||
export default function InstallAppButton(props: InstallAppButtonProps) {
|
||||
const mutation = useAddAppMutation("closecom_other_calendar");
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.render({
|
||||
onClick() {
|
||||
mutation.mutate("");
|
||||
},
|
||||
loading: mutation.isLoading,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { default as InstallAppButton } from "./InstallAppButton";
|
||||
@@ -1,18 +0,0 @@
|
||||
import type { InstallAppButtonProps } from "@calcom/app-store/types";
|
||||
|
||||
import useAddAppMutation from "../../_utils/useAddAppMutation";
|
||||
|
||||
export default function InstallAppButton(props: InstallAppButtonProps) {
|
||||
const mutation = useAddAppMutation("exchange_calendar");
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.render({
|
||||
onClick() {
|
||||
mutation.mutate("");
|
||||
},
|
||||
loading: mutation.isLoading,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { default as InstallAppButton } from "./InstallAppButton";
|
||||
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"name": "Google Tag Manager",
|
||||
"slug": "google-tag-manager",
|
||||
"type": "google-tag-manager_analytics",
|
||||
"imageSrc": "icon.svg",
|
||||
"logo": "icon.svg",
|
||||
"url": "https://tagmanager.google.com",
|
||||
"variant": "analytics",
|
||||
"categories": [
|
||||
"analytics"
|
||||
],
|
||||
"publisher": "Black Lemon",
|
||||
"email": "support@blacklemon.dk",
|
||||
"description": "App to install Google Tag Manager",
|
||||
"extendsFeature": "EventType",
|
||||
"appData": {
|
||||
"tag": {
|
||||
"scripts": [
|
||||
{
|
||||
"content":"(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer','GTM-{TRACKING_ID}');"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"isTemplate": false,
|
||||
"__createdUsingCli": true,
|
||||
"__template": "booking-pages-tag"
|
||||
}
|
||||
|
Before Width: | Height: | Size: 2.2 MiB |
@@ -1 +0,0 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 122.87 122.89"><defs><style>.cls-1{fill:#8ab4f8;}.cls-2{fill:#4285f4;}.cls-3{fill:#246fdb;}</style></defs><title>google-tag-manager</title><polygon class="cls-1" points="72.13 118.22 50.8 96.86 96.63 50.56 118.35 72.28 72.13 118.22 72.13 118.22"/><path class="cls-2" d="M72.29,26.23,50.56,4.5,4.5,50.56a15.35,15.35,0,0,0,0,21.71h0l46.06,46.06,21.31-21.4L37.07,61.45,72.29,26.23Z"/><path class="cls-1" d="M118.35,50.56,72.29,4.5A15.36,15.36,0,0,0,50.56,26.22L96.65,72.28a15.36,15.36,0,0,0,21.72-21.72Z"/><path class="cls-3" d="M76.48,107.66a15.23,15.23,0,1,0-15.22,15.23,15.22,15.22,0,0,0,15.22-15.23Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 695 B |
@@ -1,18 +0,0 @@
|
||||
import type { InstallAppButtonProps } from "@calcom/app-store/types";
|
||||
|
||||
import useAddAppMutation from "../../_utils/useAddAppMutation";
|
||||
|
||||
export default function InstallAppButton(props: InstallAppButtonProps) {
|
||||
const mutation = useAddAppMutation("google_calendar");
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.render({
|
||||
onClick() {
|
||||
mutation.mutate("");
|
||||
},
|
||||
loading: mutation.isLoading,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { default as InstallAppButton } from "./InstallAppButton";
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
items:
|
||||
- 1.jpg
|
||||
- 2.jpg
|
||||
---
|
||||
|
||||
App to install Google Tag Manager. Google Tag Manager is a tag management system that has the same functionality as the Google tag and lets you configure and instantly deploy tags on your website or mobile app from an easy-to-use web-based interface.
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "Google Tag Manager",
|
||||
"slug": "gtm",
|
||||
"type": "gtm_analytics",
|
||||
"imageSrc": "icon.svg",
|
||||
"logo": "icon.svg",
|
||||
"url": "https://tagmanager.google.com",
|
||||
"variant": "analytics",
|
||||
"categories": ["analytics"],
|
||||
"publisher": "Black Lemon",
|
||||
"email": "support@blacklemon.dk",
|
||||
"description": "App to install Google Tag Manager",
|
||||
"extendsFeature": "EventType",
|
||||
"appData": {
|
||||
"tag": {
|
||||
"scripts": [
|
||||
{
|
||||
"content": "(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer','GTM-{TRACKING_ID}');"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"isTemplate": false,
|
||||
"__createdUsingCli": true,
|
||||
"__template": "booking-pages-tag"
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"name": "@calcom/google-tag-manager",
|
||||
"name": "@calcom/gtm",
|
||||
"version": "0.0.0",
|
||||
"main": "./index.ts",
|
||||
"dependencies": {
|
||||
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 136 KiB |
@@ -0,0 +1,13 @@
|
||||
<svg width="123" height="123" viewBox="0 0 123 123" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_1_2)">
|
||||
<path d="M72.13 118.22L50.8 96.86L96.63 50.56L118.35 72.28L72.13 118.22Z" fill="#8AB4F8"/>
|
||||
<path d="M72.29 26.23L50.56 4.5L4.5 50.56C3.07435 51.9854 1.94344 53.6777 1.17187 55.5402C0.400302 57.4027 0.00317764 59.399 0.00317764 61.415C0.00317764 63.431 0.400302 65.4273 1.17187 67.2898C1.94344 69.1523 3.07435 70.8446 4.5 72.27L50.56 118.33L71.87 96.93L37.07 61.45L72.29 26.23Z" fill="#4285F4"/>
|
||||
<path d="M118.35 50.56L72.29 4.5C69.4097 1.61842 65.5028 -0.000957846 61.4285 -0.0018955C57.3543 -0.00283315 53.4466 1.61475 50.565 4.495C47.6834 7.37525 46.064 11.2822 46.0631 15.3565C46.0622 19.4307 47.6798 23.3384 50.56 26.22L96.65 72.28C99.5445 75.0911 103.429 76.6503 107.464 76.6208C111.498 76.5914 115.359 74.9755 118.212 72.1224C121.065 69.2693 122.681 65.4082 122.711 61.3735C122.74 57.3388 121.181 53.4545 118.37 50.56H118.35Z" fill="#8AB4F8"/>
|
||||
<path d="M76.48 107.66C76.48 104.647 75.5865 101.702 73.9126 99.1976C72.2386 96.6928 69.8595 94.7408 67.076 93.5884C64.2925 92.4359 61.2297 92.1349 58.2751 92.7234C55.3205 93.3118 52.6068 94.7633 50.4772 96.8943C48.3477 99.0253 46.8979 101.74 46.3114 104.695C45.7249 107.65 46.0279 110.712 47.1822 113.495C48.3364 116.278 50.2901 118.656 52.7959 120.328C55.3018 122 58.2474 122.892 61.26 122.89C63.2596 122.89 65.2395 122.496 67.0868 121.73C68.934 120.965 70.6123 119.843 72.0257 118.429C73.4391 117.014 74.56 115.335 75.3243 113.488C76.0886 111.64 76.4813 109.66 76.48 107.66Z" fill="#246FDB"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1_2">
|
||||
<rect width="122.87" height="122.89" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -1,18 +0,0 @@
|
||||
import type { InstallAppButtonProps } from "@calcom/app-store/types";
|
||||
|
||||
import useAddAppMutation from "../../_utils/useAddAppMutation";
|
||||
|
||||
export default function InstallAppButton(props: InstallAppButtonProps) {
|
||||
const mutation = useAddAppMutation("hubspot_other_calendar");
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.render({
|
||||
onClick() {
|
||||
mutation.mutate("");
|
||||
},
|
||||
loading: mutation.isLoading,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { default as InstallAppButton } from "./InstallAppButton";
|
||||
@@ -1,18 +0,0 @@
|
||||
import type { InstallAppButtonProps } from "@calcom/app-store/types";
|
||||
|
||||
import useAddAppMutation from "../../_utils/useAddAppMutation";
|
||||
|
||||
export default function InstallAppButton(props: InstallAppButtonProps) {
|
||||
const mutation = useAddAppMutation("huddle01_video");
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.render({
|
||||
onClick() {
|
||||
mutation.mutate("");
|
||||
},
|
||||
loading: mutation.isLoading,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { default as InstallAppButton } from "./InstallAppButton";
|
||||
@@ -1,18 +0,0 @@
|
||||
import type { InstallAppButtonProps } from "@calcom/app-store/types";
|
||||
|
||||
import useAddAppMutation from "../../_utils/useAddAppMutation";
|
||||
|
||||
export default function InstallAppButton(props: InstallAppButtonProps) {
|
||||
const mutation = useAddAppMutation("jitsi_video");
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.render({
|
||||
onClick() {
|
||||
mutation.mutate("");
|
||||
},
|
||||
loading: mutation.isLoading,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { default as InstallAppButton } from "./InstallAppButton";
|
||||
@@ -1,18 +0,0 @@
|
||||
import type { InstallAppButtonProps } from "@calcom/app-store/types";
|
||||
|
||||
import useAddAppMutation from "../../_utils/useAddAppMutation";
|
||||
|
||||
export default function InstallAppButton(props: InstallAppButtonProps) {
|
||||
const mutation = useAddAppMutation("lark_calendar");
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.render({
|
||||
onClick() {
|
||||
mutation.mutate("");
|
||||
},
|
||||
loading: mutation.isLoading,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { default as InstallAppButton } from "./InstallAppButton";
|
||||
@@ -20,7 +20,6 @@ export const metadata = {
|
||||
url: "https://cal.com/",
|
||||
verified: true,
|
||||
email: "help@cal.com",
|
||||
dirName: "office365calendar",
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import type { InstallAppButtonProps } from "@calcom/app-store/types";
|
||||
|
||||
import useAddAppMutation from "../../_utils/useAddAppMutation";
|
||||
|
||||
export default function InstallAppButton(props: InstallAppButtonProps) {
|
||||
const mutation = useAddAppMutation("office365_calendar");
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.render({
|
||||
onClick() {
|
||||
mutation.mutate("");
|
||||
},
|
||||
loading: mutation.isLoading,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { default as InstallAppButton } from "./InstallAppButton";
|
||||
@@ -22,5 +22,6 @@
|
||||
"type": "integrations:office365_video",
|
||||
"label": "MS Teams"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dirName": "office365video"
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import type { InstallAppButtonProps } from "@calcom/app-store/types";
|
||||
|
||||
import useAddAppMutation from "../../_utils/useAddAppMutation";
|
||||
|
||||
export default function InstallAppButton(props: InstallAppButtonProps) {
|
||||
const mutation = useAddAppMutation("riverside_video");
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.render({
|
||||
onClick() {
|
||||
mutation.mutate("");
|
||||
},
|
||||
loading: mutation.isLoading,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { default as InstallAppButton } from "./InstallAppButton";
|
||||
@@ -44,30 +44,28 @@ export default function FormInputFields(props: Props) {
|
||||
{field.label}
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex rounded-sm">
|
||||
<Component
|
||||
value={response[field.id]?.value}
|
||||
placeholder={field.placeholder ?? ""}
|
||||
// required property isn't accepted by query-builder types
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
/* @ts-ignore */
|
||||
required={!!field.required}
|
||||
listValues={options}
|
||||
data-testid="form-field"
|
||||
setValue={(value) => {
|
||||
setResponse((response) => {
|
||||
response = response || {};
|
||||
return {
|
||||
...response,
|
||||
[field.id]: {
|
||||
label: field.label,
|
||||
value,
|
||||
},
|
||||
};
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Component
|
||||
value={response[field.id]?.value}
|
||||
placeholder={field.placeholder ?? ""}
|
||||
// required property isn't accepted by query-builder types
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
/* @ts-ignore */
|
||||
required={!!field.required}
|
||||
listValues={options}
|
||||
data-testid="form-field"
|
||||
setValue={(value) => {
|
||||
setResponse((response) => {
|
||||
response = response || {};
|
||||
return {
|
||||
...response,
|
||||
[field.id]: {
|
||||
label: field.label,
|
||||
value,
|
||||
},
|
||||
};
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -103,7 +103,7 @@ const widgets: Widgets & { [key in keyof Widgets]: Widgets[key] & { type: string
|
||||
props.setValue(val);
|
||||
}}
|
||||
containerClassName="w-full"
|
||||
className="dark:placeholder:text-darkgray-600 focus:border-brand dark:border-darkgray-300 dark:text-darkgray-900 block w-full rounded-md border-gray-300 text-sm focus:ring-black disabled:bg-gray-200 disabled:hover:cursor-not-allowed dark:bg-transparent dark:selection:bg-green-500 disabled:dark:text-gray-500"
|
||||
className="dark:placeholder:text-darkgray-600 focus:border-brand border-subtle dark:text-darkgray-900 block w-full rounded-md border-gray-300 text-sm focus:ring-black disabled:bg-gray-200 disabled:hover:cursor-not-allowed dark:bg-transparent dark:selection:bg-green-500 disabled:dark:text-gray-500"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -120,7 +120,7 @@ function RoutingForm({ form, profile, ...restProps }: inferSSRProps<typeof getSe
|
||||
</Head>
|
||||
<div className={classNames("mx-auto my-0 max-w-3xl", isEmbed ? "" : "md:my-24")}>
|
||||
<div className="w-full max-w-4xl ltr:mr-2 rtl:ml-2">
|
||||
<div className="main border-bookinglightest dark:bg-darkgray-100 sm:dark:border-darkgray-300 bg-default mx-0 rounded-md p-4 py-6 sm:-mx-4 sm:px-8 md:border">
|
||||
<div className="main border-bookinglightest dark:bg-darkgray-100 sm:border-subtle bg-default mx-0 rounded-md p-4 py-6 sm:-mx-4 sm:px-8 md:border">
|
||||
<Toaster position="bottom-right" />
|
||||
|
||||
<form onSubmit={handleOnSubmit}>
|
||||
@@ -150,7 +150,7 @@ function RoutingForm({ form, profile, ...restProps }: inferSSRProps<typeof getSe
|
||||
) : (
|
||||
<div className="mx-auto my-0 max-w-3xl md:my-24">
|
||||
<div className="w-full max-w-4xl ltr:mr-2 rtl:ml-2">
|
||||
<div className="main dark:bg-darkgray-100 sm:dark:border-darkgray-300 bg-default -mx-4 rounded-md border border-neutral-200 p-4 py-6 sm:mx-0 sm:px-8">
|
||||
<div className="main dark:bg-darkgray-100 sm:border-subtle bg-default -mx-4 rounded-md border border-neutral-200 p-4 py-6 sm:mx-0 sm:px-8">
|
||||
<div className="text-emphasis">{customPageMessage}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,18 +7,24 @@ import AppCard from "@calcom/app-store/_components/AppCard";
|
||||
import type { EventTypeAppCardComponent } from "@calcom/app-store/types";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Alert, TextField } from "@calcom/ui";
|
||||
import { Alert, TextField, Select } from "@calcom/ui";
|
||||
|
||||
import { paymentOptions } from "../lib/constants";
|
||||
import type { appDataSchema } from "../zod";
|
||||
|
||||
type Option = { value: string; label: string };
|
||||
|
||||
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) {
|
||||
const { asPath } = useRouter();
|
||||
const [getAppData, setAppData] = useAppContextWithSchema<typeof appDataSchema>();
|
||||
const price = getAppData("price");
|
||||
const currency = getAppData("currency");
|
||||
const paymentOption = getAppData("paymentOption");
|
||||
const paymentOptionSelectValue = paymentOptions.find((option) => paymentOption === option.value);
|
||||
const [requirePayment, setRequirePayment] = useState(getAppData("enabled"));
|
||||
const { t } = useLocale();
|
||||
const recurringEventDefined = eventType.recurringEvent?.count !== undefined;
|
||||
const seatsEnabled = !!eventType.seatsPerTimeSlot;
|
||||
const getCurrencySymbol = (locale: string, currency: string) =>
|
||||
(0)
|
||||
.toLocaleString(locale, {
|
||||
@@ -54,22 +60,43 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
||||
<Alert className="mt-2" severity="warning" title={t("warning_recurring_event_payment")} />
|
||||
) : (
|
||||
requirePayment && (
|
||||
<div className="mt-2 block items-center sm:flex">
|
||||
<TextField
|
||||
label=""
|
||||
addOnLeading={<>{currency ? getCurrencySymbol("en", currency) : ""}</>}
|
||||
step="0.01"
|
||||
min="0.5"
|
||||
type="number"
|
||||
required
|
||||
className="border-default block w-full rounded-sm pl-2 pr-12 text-sm"
|
||||
placeholder="Price"
|
||||
onChange={(e) => {
|
||||
setAppData("price", Number(e.target.value) * 100);
|
||||
}}
|
||||
value={price > 0 ? price / 100 : undefined}
|
||||
/>
|
||||
</div>
|
||||
<>
|
||||
<div className="mt-2 block items-center justify-start sm:flex sm:space-x-2">
|
||||
<TextField
|
||||
label=""
|
||||
className="h-[38px]"
|
||||
addOnLeading={<>{currency ? getCurrencySymbol("en", currency) : ""}</>}
|
||||
addOnClassname="h-[38px]"
|
||||
step="0.01"
|
||||
min="0.5"
|
||||
type="number"
|
||||
required
|
||||
placeholder="Price"
|
||||
onChange={(e) => {
|
||||
setAppData("price", Number(e.target.value) * 100);
|
||||
}}
|
||||
value={price > 0 ? price / 100 : undefined}
|
||||
/>
|
||||
<Select<Option>
|
||||
defaultValue={
|
||||
paymentOptionSelectValue
|
||||
? { ...paymentOptionSelectValue, label: t(paymentOptionSelectValue.label) }
|
||||
: { ...paymentOptions[0], label: t(paymentOptions[0].label) }
|
||||
}
|
||||
options={paymentOptions.map((option) => {
|
||||
return { ...option, label: t(option.label) || option.label };
|
||||
})}
|
||||
onChange={(input) => {
|
||||
if (input) setAppData("paymentOption", input.value);
|
||||
}}
|
||||
className="mb-1 h-[38px] w-full"
|
||||
isDisabled={seatsEnabled}
|
||||
/>
|
||||
</div>
|
||||
{seatsEnabled && paymentOption === "HOLD" && (
|
||||
<Alert className="mt-2" severity="warning" title={t("seats_and_no_show_fee_error")} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Booking, Payment, Prisma } from "@prisma/client";
|
||||
import type { Booking, Payment, Prisma, PaymentOption } from "@prisma/client";
|
||||
import Stripe from "stripe";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import z from "zod";
|
||||
@@ -9,8 +9,10 @@ import { getErrorFromUnknown } from "@calcom/lib/errors";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
|
||||
import { paymentOptionEnum } from "../zod";
|
||||
import { createPaymentLink } from "./client";
|
||||
import type { StripePaymentData } from "./server";
|
||||
import { retrieveOrCreateStripeCustomerByEmail } from "./customer";
|
||||
import type { StripeSetupIntentData, StripePaymentData } from "./server";
|
||||
|
||||
const stripeCredentialKeysSchema = z.object({
|
||||
stripe_user_id: z.string(),
|
||||
@@ -36,11 +38,19 @@ export class PaymentService implements IAbstractPaymentService {
|
||||
});
|
||||
}
|
||||
|
||||
/* This method is for creating charges at the time of booking */
|
||||
async create(
|
||||
payment: Pick<Prisma.PaymentUncheckedCreateInput, "amount" | "currency">,
|
||||
bookingId: Booking["id"]
|
||||
bookingId: Booking["id"],
|
||||
bookerEmail: string,
|
||||
paymentOption: PaymentOption
|
||||
) {
|
||||
try {
|
||||
// Ensure that the payment service can support the passed payment option
|
||||
if (paymentOptionEnum.parse(paymentOption) !== "ON_BOOKING") {
|
||||
throw new Error("Payment option is not compatible with create method");
|
||||
}
|
||||
|
||||
// Load stripe keys
|
||||
const stripeAppKeys = await prisma?.app.findFirst({
|
||||
select: {
|
||||
@@ -57,11 +67,17 @@ export class PaymentService implements IAbstractPaymentService {
|
||||
);
|
||||
const paymentFee = Math.round(payment.amount * payment_fee_percentage + payment_fee_fixed);
|
||||
|
||||
const customer = await retrieveOrCreateStripeCustomerByEmail(
|
||||
bookerEmail,
|
||||
this.credentials.stripe_user_id
|
||||
);
|
||||
|
||||
const params: Stripe.PaymentIntentCreateParams = {
|
||||
amount: payment.amount,
|
||||
currency: this.credentials.default_currency,
|
||||
payment_method_types: ["card"],
|
||||
application_fee_amount: paymentFee,
|
||||
customer: customer.id,
|
||||
};
|
||||
|
||||
const paymentIntent = await this.stripe.paymentIntents.create(params, {
|
||||
@@ -92,6 +108,7 @@ export class PaymentService implements IAbstractPaymentService {
|
||||
fee: paymentFee,
|
||||
refunded: false,
|
||||
success: false,
|
||||
paymentOption: paymentOption || "ON_BOOKING",
|
||||
},
|
||||
});
|
||||
if (!paymentData) {
|
||||
@@ -104,6 +121,161 @@ export class PaymentService implements IAbstractPaymentService {
|
||||
}
|
||||
}
|
||||
|
||||
async collectCard(
|
||||
payment: Pick<Prisma.PaymentUncheckedCreateInput, "amount" | "currency">,
|
||||
bookingId: Booking["id"],
|
||||
bookerEmail: string,
|
||||
paymentOption: PaymentOption
|
||||
): Promise<Payment> {
|
||||
// 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",
|
||||
},
|
||||
});
|
||||
|
||||
// Parse keys with zod
|
||||
const { client_id, payment_fee_fixed, payment_fee_percentage } = stripeAppKeysSchema.parse(
|
||||
stripeAppKeys?.keys
|
||||
);
|
||||
const paymentFee = Math.round(payment.amount * payment_fee_percentage + payment_fee_fixed);
|
||||
|
||||
const customer = await retrieveOrCreateStripeCustomerByEmail(
|
||||
bookerEmail,
|
||||
this.credentials.stripe_user_id
|
||||
);
|
||||
|
||||
const params = {
|
||||
customer: customer.id,
|
||||
payment_method_types: ["card"],
|
||||
metadata: {
|
||||
bookingId,
|
||||
},
|
||||
};
|
||||
|
||||
const setupIntent = await this.stripe.setupIntents.create(params, {
|
||||
stripeAccount: this.credentials.stripe_user_id,
|
||||
});
|
||||
|
||||
const paymentData = await prisma?.payment.create({
|
||||
data: {
|
||||
uid: uuidv4(),
|
||||
app: {
|
||||
connect: {
|
||||
slug: "stripe",
|
||||
},
|
||||
},
|
||||
booking: {
|
||||
connect: {
|
||||
id: bookingId,
|
||||
},
|
||||
},
|
||||
amount: payment.amount,
|
||||
currency: payment.currency,
|
||||
externalId: setupIntent.id,
|
||||
|
||||
data: Object.assign(
|
||||
{},
|
||||
{
|
||||
setupIntent,
|
||||
stripe_publishable_key: this.credentials.stripe_publishable_key,
|
||||
stripeAccount: this.credentials.stripe_user_id,
|
||||
}
|
||||
) as unknown as Prisma.InputJsonValue,
|
||||
fee: paymentFee,
|
||||
refunded: false,
|
||||
success: false,
|
||||
paymentOption: paymentOption || "ON_BOOKING",
|
||||
},
|
||||
});
|
||||
|
||||
return paymentData;
|
||||
}
|
||||
|
||||
async chargeCard(payment: Payment, bookingId: Booking["id"]): Promise<Payment> {
|
||||
try {
|
||||
const stripeAppKeys = await prisma?.app.findFirst({
|
||||
select: {
|
||||
keys: true,
|
||||
},
|
||||
where: {
|
||||
slug: "stripe",
|
||||
},
|
||||
});
|
||||
|
||||
const paymentObject = payment.data as unknown as StripeSetupIntentData;
|
||||
|
||||
const setupIntent = paymentObject.setupIntent;
|
||||
|
||||
// Parse keys with zod
|
||||
const { client_id, payment_fee_fixed, payment_fee_percentage } = stripeAppKeysSchema.parse(
|
||||
stripeAppKeys?.keys
|
||||
);
|
||||
|
||||
const paymentFee = Math.round(payment.amount * payment_fee_percentage + payment_fee_fixed);
|
||||
|
||||
// Ensure that the stripe customer & payment method still exists
|
||||
const customer = await this.stripe.customers.retrieve(setupIntent.customer as string, {
|
||||
stripeAccount: this.credentials.stripe_user_id,
|
||||
});
|
||||
const paymentMethod = await this.stripe.paymentMethods.retrieve(setupIntent.payment_method as string, {
|
||||
stripeAccount: this.credentials.stripe_user_id,
|
||||
});
|
||||
|
||||
if (!customer) {
|
||||
throw new Error(`Stripe customer does not exist for setupIntent ${setupIntent.id}`);
|
||||
}
|
||||
|
||||
if (!paymentMethod) {
|
||||
throw new Error(`Stripe paymentMethod does not exist for setupIntent ${setupIntent.id}`);
|
||||
}
|
||||
|
||||
const params = {
|
||||
amount: payment.amount,
|
||||
currency: payment.currency,
|
||||
application_fee_amount: paymentFee,
|
||||
customer: setupIntent.customer as string,
|
||||
payment_method: setupIntent.payment_method as string,
|
||||
off_session: true,
|
||||
confirm: true,
|
||||
};
|
||||
|
||||
const paymentIntent = await this.stripe.paymentIntents.create(params, {
|
||||
stripeAccount: this.credentials.stripe_user_id,
|
||||
});
|
||||
|
||||
const paymentData = await prisma.payment.update({
|
||||
where: {
|
||||
id: payment.id,
|
||||
},
|
||||
data: {
|
||||
success: true,
|
||||
data: {
|
||||
...paymentObject,
|
||||
paymentIntent,
|
||||
} as unknown as Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
|
||||
if (!paymentData) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
return paymentData;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw new Error("Payment could not be created");
|
||||
}
|
||||
}
|
||||
|
||||
async update(): Promise<Payment> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
@@ -166,6 +338,9 @@ export class PaymentService implements IAbstractPaymentService {
|
||||
email: booking.user?.email,
|
||||
date: booking.startTime.toISOString(),
|
||||
}),
|
||||
paymentOption: paymentData.paymentOption || "ON_BOOKING",
|
||||
amount: paymentData.amount,
|
||||
currency: paymentData.currency,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
export const PREMIUM_MONTHLY_PLAN_PRICE = process.env.NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE_MONTHLY || "";
|
||||
export const PREMIUM_PLAN_PRODUCT_ID = process.env.NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRODUCT_ID || "";
|
||||
export const STRIPE_TEAM_MONTHLY_PRICE_ID = process.env.NEXT_PUBLIC_STRIPE_TEAM_MONTHLY_PRICE_ID || "";
|
||||
|
||||
export const paymentOptions = [
|
||||
{
|
||||
label: "on_booking_option",
|
||||
value: "ON_BOOKING",
|
||||
},
|
||||
{
|
||||
label: "hold_option",
|
||||
value: "HOLD",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -82,3 +82,27 @@ export async function deleteStripeCustomer(user: UserType): Promise<string | nul
|
||||
|
||||
return deletedCustomer.id;
|
||||
}
|
||||
|
||||
export async function retrieveOrCreateStripeCustomerByEmail(email: string, stripeAccountId: string) {
|
||||
const customer = await stripe.customers.list(
|
||||
{
|
||||
email,
|
||||
limit: 1,
|
||||
},
|
||||
{
|
||||
stripeAccount: stripeAccountId,
|
||||
}
|
||||
);
|
||||
|
||||
if (customer.data[0]?.id) {
|
||||
return customer.data[0];
|
||||
} else {
|
||||
const newCustomer = await stripe.customers.create(
|
||||
{ email },
|
||||
{
|
||||
stripeAccount: stripeAccountId,
|
||||
}
|
||||
);
|
||||
return newCustomer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,11 @@ export type StripePaymentData = Stripe.Response<Stripe.PaymentIntent> & {
|
||||
stripeAccount: string;
|
||||
};
|
||||
|
||||
export type StripeSetupIntentData = {
|
||||
setupIntent: Stripe.Response<Stripe.SetupIntent>;
|
||||
paymentIntent?: StripePaymentData;
|
||||
};
|
||||
|
||||
export const stripeOAuthTokenSchema = z.object({
|
||||
access_token: z.string().optional(),
|
||||
scope: z.string().optional(),
|
||||
|
||||
@@ -1,11 +1,22 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { eventTypeAppCardZod } from "../eventTypeAppCardZod";
|
||||
import { paymentOptions } from "./lib/constants";
|
||||
|
||||
// Extract the payment options enum from paymentOptions
|
||||
// https://stackoverflow.com/a/73825370
|
||||
type PaymentOption = (typeof paymentOptions)[number]["value"];
|
||||
const VALUES: [PaymentOption, ...PaymentOption[]] = [
|
||||
paymentOptions[0].value,
|
||||
...paymentOptions.slice(1).map((option) => option.value),
|
||||
];
|
||||
export const paymentOptionEnum = z.enum(VALUES);
|
||||
|
||||
export const appDataSchema = eventTypeAppCardZod.merge(
|
||||
z.object({
|
||||
price: z.number(),
|
||||
currency: z.string(),
|
||||
paymentOption: paymentOptionEnum.optional(),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -14,6 +25,6 @@ export const appKeysSchema = z.object({
|
||||
client_secret: z.string().startsWith("sk_").min(1),
|
||||
public_key: z.string().startsWith("pk_").min(1),
|
||||
webhook_secret: z.string().startsWith("whsec_").min(1),
|
||||
payment_fee_fixed: z.number().int().min(0),
|
||||
payment_fee_fixed: z.number().min(0),
|
||||
payment_fee_percentage: z.number().min(0),
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
items:
|
||||
- /api/app-store/sylaps/sylaps1.png
|
||||
- 1.jpg
|
||||
---
|
||||
|
||||
Free Audio and Video Conferencing, Online Collaboration, Screen Sharing on web browser, mobile and desktop.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"/*": "Don't modify slug - If required, do it using cli edit command",
|
||||
"name": "Sylaps",
|
||||
"title": "Sylaps",
|
||||
"slug": "sylaps",
|
||||
"slug": "sylapsvideo",
|
||||
"type": "sylaps_video",
|
||||
"imageSrc": "icon.svg",
|
||||
"logo": "icon.svg",
|
||||
|
||||
|
After Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 172 KiB |
@@ -1,18 +0,0 @@
|
||||
import type { InstallAppButtonProps } from "@calcom/app-store/types";
|
||||
|
||||
import useAddAppMutation from "../../_utils/useAddAppMutation";
|
||||
|
||||
export default function InstallAppButton(props: InstallAppButtonProps) {
|
||||
const mutation = useAddAppMutation("tandem_video");
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.render({
|
||||
onClick() {
|
||||
mutation.mutate("");
|
||||
},
|
||||
loading: mutation.isLoading,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { default as InstallAppButton } from "./InstallAppButton";
|
||||
@@ -30,7 +30,7 @@ export type EventTypeAppCardComponentProps = {
|
||||
// Limit what data should be accessible to apps
|
||||
eventType: Pick<
|
||||
z.infer<typeof _EventTypeModel>,
|
||||
"id" | "title" | "description" | "teamId" | "length" | "recurringEvent"
|
||||
"id" | "title" | "description" | "teamId" | "length" | "recurringEvent" | "seatsPerTimeSlot"
|
||||
> & {
|
||||
URL: string;
|
||||
};
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import type { InstallAppButtonProps } from "@calcom/app-store/types";
|
||||
|
||||
import useAddAppMutation from "../../_utils/useAddAppMutation";
|
||||
|
||||
export default function InstallAppButton(props: InstallAppButtonProps) {
|
||||
const mutation = useAddAppMutation("whereby_video");
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.render({
|
||||
onClick() {
|
||||
mutation.mutate("");
|
||||
},
|
||||
loading: mutation.isLoading,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { default as InstallAppButton } from "./InstallAppButton";
|
||||
@@ -1,18 +0,0 @@
|
||||
import type { InstallAppButtonProps } from "@calcom/app-store/types";
|
||||
|
||||
import useAddAppMutation from "../../_utils/useAddAppMutation";
|
||||
|
||||
export default function InstallAppButton(props: InstallAppButtonProps) {
|
||||
const mutation = useAddAppMutation("wipemycal_other");
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.render({
|
||||
onClick() {
|
||||
mutation.mutate("");
|
||||
},
|
||||
loading: mutation.isLoading,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import type { InstallAppButtonProps } from "@calcom/app-store/types";
|
||||
|
||||
import useAddAppMutation from "../../_utils/useAddAppMutation";
|
||||
|
||||
export default function InstallAppButton(props: InstallAppButtonProps) {
|
||||
const mutation = useAddAppMutation("zapier_automation");
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.render({
|
||||
onClick() {
|
||||
mutation.mutate("");
|
||||
},
|
||||
loading: mutation.isLoading,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { default as InstallAppButton } from "./InstallAppButton";
|
||||
@@ -1,18 +0,0 @@
|
||||
import type { InstallAppButtonProps } from "@calcom/app-store/types";
|
||||
|
||||
import useAddAppMutation from "../../_utils/useAddAppMutation";
|
||||
|
||||
export default function InstallAppButton(props: InstallAppButtonProps) {
|
||||
const mutation = useAddAppMutation("zoom_video");
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.render({
|
||||
onClick() {
|
||||
mutation.mutate("");
|
||||
},
|
||||
loading: mutation.isLoading,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { default as InstallAppButton } from "./InstallAppButton";
|
||||
@@ -0,0 +1,20 @@
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode; // React.ReactNode
|
||||
fallback?: JSX.Element | null; // JSX.Element
|
||||
}
|
||||
|
||||
const NoSSR = ({ children, fallback = null }: Props) => {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => setMounted(true), []);
|
||||
|
||||
if (!mounted) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export default NoSSR;
|
||||
@@ -21,6 +21,7 @@ import type { Feedback } from "./templates/feedback-email";
|
||||
import FeedbackEmail from "./templates/feedback-email";
|
||||
import type { PasswordReset } from "./templates/forgot-password-email";
|
||||
import ForgotPasswordEmail from "./templates/forgot-password-email";
|
||||
import NoShowFeeChargedEmail from "./templates/no-show-fee-charged-email";
|
||||
import OrganizerAttendeeCancelledSeatEmail from "./templates/organizer-attendee-cancelled-seat-email";
|
||||
import OrganizerCancelledEmail from "./templates/organizer-cancelled-email";
|
||||
import OrganizerLocationChangeEmail from "./templates/organizer-location-change-email";
|
||||
@@ -285,3 +286,7 @@ export const sendDisabledAppEmail = async ({
|
||||
}) => {
|
||||
await sendEmail(() => new DisabledAppEmail(email, appName, appType, t, title, eventTypeId));
|
||||
};
|
||||
|
||||
export const sendNoShowFeeChargedEmail = async (attendee: Person, evt: CalendarEvent) => {
|
||||
await sendEmail(() => new NoShowFeeChargedEmail(evt, attendee));
|
||||
};
|
||||
|
||||
@@ -4,7 +4,8 @@ import EmailBodyLogo from "./EmailBodyLogo";
|
||||
import EmailHead from "./EmailHead";
|
||||
import EmailScheduledBodyHeaderContent from "./EmailScheduledBodyHeaderContent";
|
||||
import EmailSchedulingBodyDivider from "./EmailSchedulingBodyDivider";
|
||||
import EmailSchedulingBodyHeader, { BodyHeadType } from "./EmailSchedulingBodyHeader";
|
||||
import type { BodyHeadType } from "./EmailSchedulingBodyHeader";
|
||||
import EmailSchedulingBodyHeader from "./EmailSchedulingBodyHeader";
|
||||
import RawHtml from "./RawHtml";
|
||||
import Row from "./Row";
|
||||
|
||||
@@ -20,7 +21,7 @@ export const BaseEmailHtml = (props: {
|
||||
callToAction?: React.ReactNode;
|
||||
subject: string;
|
||||
title?: string;
|
||||
subtitle?: React.ReactNode;
|
||||
subtitle?: React.ReactNode | string;
|
||||
headerType?: BodyHeadType;
|
||||
}) => {
|
||||
return (
|
||||
|
||||
@@ -13,12 +13,22 @@ function ManageLink(props: React.ComponentProps<typeof AttendeeScheduledEmail>)
|
||||
);
|
||||
}
|
||||
|
||||
export const AttendeeAwaitingPaymentEmail = (props: React.ComponentProps<typeof AttendeeScheduledEmail>) => (
|
||||
<AttendeeScheduledEmail
|
||||
title="meeting_awaiting_payment"
|
||||
headerType="calendarCircle"
|
||||
subject="awaiting_payment_subject"
|
||||
callToAction={<ManageLink {...props} />}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
export const AttendeeAwaitingPaymentEmail = (props: React.ComponentProps<typeof AttendeeScheduledEmail>) => {
|
||||
return props.calEvent.paymentInfo?.paymentOption === "HOLD" ? (
|
||||
<AttendeeScheduledEmail
|
||||
title="meeting_awaiting_payment_method"
|
||||
headerType="calendarCircle"
|
||||
subject="awaiting_payment_subject"
|
||||
callToAction={<ManageLink {...props} />}
|
||||
{...props}
|
||||
/>
|
||||
) : (
|
||||
<AttendeeScheduledEmail
|
||||
title="meeting_awaiting_payment"
|
||||
headerType="calendarCircle"
|
||||
subject="awaiting_payment_subject"
|
||||
callToAction={<ManageLink {...props} />}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -80,6 +80,16 @@ export const BaseScheduledEmail = (
|
||||
<Info label={t("additional_notes")} description={props.calEvent.additionalNotes} withSpacer />
|
||||
{props.includeAppsStatus && <AppsStatus calEvent={props.calEvent} t={t} />}
|
||||
<UserFieldsResponses calEvent={props.calEvent} />
|
||||
{props.calEvent.paymentInfo?.amount && (
|
||||
<Info
|
||||
label={props.calEvent.paymentInfo?.paymentOption === "HOLD" ? t("no_show_fee") : t("price")}
|
||||
description={new Intl.NumberFormat(props.attendee.language.locale, {
|
||||
style: "currency",
|
||||
currency: props.calEvent.paymentInfo?.currency || "USD",
|
||||
}).format(props.calEvent.paymentInfo?.amount / 100.0)}
|
||||
withSpacer
|
||||
/>
|
||||
)}
|
||||
</BaseEmailHtml>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
||||
|
||||
import { BaseScheduledEmail } from "./BaseScheduledEmail";
|
||||
|
||||
export const NoShowFeeChargedEmail = (
|
||||
props: {
|
||||
calEvent: CalendarEvent;
|
||||
attendee: Person;
|
||||
} & Partial<React.ComponentProps<typeof BaseScheduledEmail>>
|
||||
) => {
|
||||
const { calEvent } = props;
|
||||
const t = props.attendee.language.translate;
|
||||
|
||||
if (!calEvent.paymentInfo?.amount) throw new Error("No payment info");
|
||||
|
||||
return (
|
||||
<BaseScheduledEmail
|
||||
title={t("no_show_fee_charged_text_body")}
|
||||
headerType="calendarCircle"
|
||||
subtitle={
|
||||
<>
|
||||
{t("no_show_fee_charged_subtitle", {
|
||||
amount: calEvent.paymentInfo.amount / 100,
|
||||
formatParams: { amount: { currency: calEvent.paymentInfo?.currency } },
|
||||
})}
|
||||
</>
|
||||
}
|
||||
timeZone={props.attendee.timeZone}
|
||||
{...props}
|
||||
t={t}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -21,4 +21,5 @@ export { OrganizerScheduledEmail } from "./OrganizerScheduledEmail";
|
||||
export { TeamInviteEmail } from "./TeamInviteEmail";
|
||||
export { BrokenIntegrationEmail } from "./BrokenIntegrationEmail";
|
||||
export { OrganizerAttendeeCancelledSeatEmail } from "./OrganizerAttendeeCancelledSeatEmail";
|
||||
export { NoShowFeeChargedEmail } from "./NoShowFeeChargedEmail";
|
||||
export * from "@calcom/app-store/routing-forms/emails/components";
|
||||
|
||||
@@ -7,7 +7,7 @@ export default class AttendeeAwaitingPaymentEmail extends AttendeeScheduledEmail
|
||||
to: `${this.attendee.name} <${this.attendee.email}>`,
|
||||
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
||||
replyTo: this.calEvent.organizer.email,
|
||||
subject: `${this.attendee.language.translate("awaiting_payment_subject", {
|
||||
subject: `${this.attendee.language.translate("complete_your_booking_subject", {
|
||||
title: this.calEvent.title,
|
||||
date: this.getFormattedDate(),
|
||||
})}`,
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { renderEmail } from "../";
|
||||
import AttendeeScheduledEmail from "./attendee-scheduled-email";
|
||||
|
||||
export default class NoShowFeeChargedEmail extends AttendeeScheduledEmail {
|
||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
||||
if (!this.calEvent.paymentInfo?.amount) throw new Error("No payment into");
|
||||
return {
|
||||
to: `${this.attendee.name} <${this.attendee.email}>`,
|
||||
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
||||
replyTo: this.calEvent.organizer.email,
|
||||
subject: `${this.attendee.language.translate("no_show_fee_charged_email_subject", {
|
||||
title: this.calEvent.title,
|
||||
date: this.getFormattedDate(),
|
||||
amount: this.calEvent.paymentInfo.amount / 100,
|
||||
formatParams: { amount: { currency: this.calEvent.paymentInfo?.currency } },
|
||||
})}`,
|
||||
html: renderEmail("NoShowFeeChargedEmail", {
|
||||
calEvent: this.calEvent,
|
||||
attendee: this.attendee,
|
||||
}),
|
||||
text: this.getTextBody("no_show_fee_charged_text_body"),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,7 @@ const IntegrationContainer = ({
|
||||
const utils = trpc.useContext();
|
||||
const [disableDialog, setDisableDialog] = useState(false);
|
||||
|
||||
const showKeyModal = () => {
|
||||
const showKeyModal = (fromEnabled?: boolean) => {
|
||||
// FIXME: This is preventing the modal from opening for apps that has null keys
|
||||
if (app.keys) {
|
||||
handleModelOpen({
|
||||
@@ -58,6 +58,8 @@ const IntegrationContainer = ({
|
||||
slug: app.slug,
|
||||
type: app.type,
|
||||
isOpen: "editKeys",
|
||||
fromEnabled,
|
||||
appName: app.name,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -99,6 +101,8 @@ const IntegrationContainer = ({
|
||||
onClick={() => {
|
||||
if (app.enabled) {
|
||||
setDisableDialog(true);
|
||||
} else if (app.keys) {
|
||||
showKeyModal(true);
|
||||
} else {
|
||||
enableAppMutation.mutate({ slug: app.slug, enabled: app.enabled });
|
||||
}
|
||||
@@ -179,9 +183,12 @@ const EditKeysModal: FC<{
|
||||
isOpen: boolean;
|
||||
keys: App["keys"];
|
||||
handleModelClose: () => void;
|
||||
fromEnabled?: boolean;
|
||||
appName?: string;
|
||||
}> = (props) => {
|
||||
const utils = trpc.useContext();
|
||||
const { t } = useLocale();
|
||||
const { dirName, slug, type, isOpen, keys, handleModelClose } = props;
|
||||
const { dirName, slug, type, isOpen, keys, handleModelClose, fromEnabled, appName } = props;
|
||||
const appKeySchema = appKeysSchemas[dirName as keyof typeof appKeysSchemas];
|
||||
|
||||
const formMethods = useForm({
|
||||
@@ -190,7 +197,8 @@ const EditKeysModal: FC<{
|
||||
|
||||
const saveKeysMutation = trpc.viewer.appsRouter.saveKeys.useMutation({
|
||||
onSuccess: () => {
|
||||
showToast(t("keys_have_been_saved"), "success");
|
||||
showToast(fromEnabled ? t("app_is_enabled", { appName }) : t("keys_have_been_saved"), "success");
|
||||
utils.viewer.appsRouter.listLocal.invalidate();
|
||||
handleModelClose();
|
||||
},
|
||||
onError: (error) => {
|
||||
@@ -211,6 +219,7 @@ const EditKeysModal: FC<{
|
||||
type,
|
||||
keys: values,
|
||||
dirName,
|
||||
fromEnabled,
|
||||
})
|
||||
}
|
||||
className="px-4 pb-4">
|
||||
@@ -251,6 +260,8 @@ interface EditModalState extends Pick<App, "keys"> {
|
||||
dirName: string;
|
||||
type: string;
|
||||
slug: string;
|
||||
fromEnabled?: boolean;
|
||||
appName?: string;
|
||||
}
|
||||
|
||||
const AdminAppsListContainer = () => {
|
||||
@@ -310,6 +321,8 @@ const AdminAppsListContainer = () => {
|
||||
isOpen={modalState.isOpen === "editKeys"}
|
||||
slug={modalState.slug}
|
||||
type={modalState.type}
|
||||
fromEnabled={modalState.fromEnabled}
|
||||
appName={modalState.appName}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||