Compare commits

..
26 Commits
Author SHA1 Message Date
Alex van Andel 1d18a09cc8 v2.7.15
release.yml / release (push) Canceled after 0s
2023-04-12 13:58:42 +02:00
Peer Richelsen 887d63ceef app store fixes (#8224) 2023-04-12 11:40:53 +00:00
Hariom Balhara 555fcc21f2 Revert color-scheme for embed (#8229) 2023-04-12 11:16:59 +00:00
Peer Richelsen f45fb03838 wip: replaced dark border with border-subtle (#8211) 2023-04-12 10:42:41 +02:00
Peer Richelsen 46611fdf77 make routing form full width (#8215) 2023-04-12 07:39:04 +05:30
Crowdin Bot c0f7c40bec New Crowdin translations by Github Action 2023-04-12 01:57:27 +00:00
Joe Au-Yeungandzomars f6f257c705 Refactor app store & fix admin apps list bugs (#7812)
* If omni install do not show app successfully installed toast

* Clean up. Up to hubspot

* Fix double click to enable apps

* Clean up rest of apps

* Create cli app add dirName

* Type fix

* Only enable apps if keys are valid

* Save dirName as slug

* Remove dirname from metadata

---------

Co-authored-by: zomars <zomars@me.com>
2023-04-11 18:56:43 -07:00
Crowdin Bot d61f50724b New Crowdin translations by Github Action 2023-04-11 22:14:21 +00:00
Alex van Andel 960aa0e00b v2.7.14
release.yml / release (push) Canceled after 0s
2023-04-12 00:10:27 +02:00
Joe Au-YeungandAlex van Andel 817e20f11e Stripe add the ability to place hold on cards (#8022)
* Add payment option to schema

* Add payment option to Stripe zod

* Set payment option on event type

* Create manual payment intent in Stripe

* Set payment option from Stripe app

* Add payment option to DB

* Pass React.ReactNode to checkbox

* Create uncaptured payment intent

* WIP

* Capture card in setup intent

* Show charge card option

* Charge card from booking page

* Bug fixes

* Clean up

* Clean up app card

* Add no-show fee messaging on booking page

* Send payment email on payment & add price

* Fix messaging

* Create no show fee charged email

* Send charge fee collected email

* Disable submit on card failure

* Clean up

* Serverside prevent charging card again if already charged

* Only confirm booking if paid for

* Type fixes

* More type fixes

* More type fixes

* Type fix

* Type fixes

* UI changes

* Payment component rework

* Update apps/web/public/static/locales/en/common.json

Co-authored-by: Alex van Andel <me@alexvanandel.com>

* Update apps/web/public/static/locales/en/common.json

Co-authored-by: Alex van Andel <me@alexvanandel.com>

* Update apps/web/components/dialog/ChargeCardDialog.tsx

Co-authored-by: Alex van Andel <me@alexvanandel.com>

* Update packages/trpc/server/routers/viewer/payments.tsx

Co-authored-by: Alex van Andel <me@alexvanandel.com>

* Revert GTM config

* Adjust payment option dropdown

* Show alert when seats are set

* Small bug fixes

* Create collect card method

* clean up

* Prevent seats & charge no-show fee to be enabled together

* Do not charge no-show fee on unconfirmed bookings

* Add check to collect card method

* Webhook send request emails

* Fix some dark mode colours

* Change awaiting payment language

* Type fixes

* Set height of Select and TextField both to 38px to fix alignment

* Fix message seats & payment error message

* Type fix

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-04-11 23:44:14 +02:00
Keith Williams af0123a606 Updated issue/PR staleness checks (#8209)
* Updated issue/PR staleness checks

* Changed order of settings to group them better
2023-04-11 22:39:53 +02:00
zomars 709bbf6626 Update yarn.lock 2023-04-11 11:41:48 -07:00
zomars 53097ee3bc Delete lockfile.yml 2023-04-11 11:40:13 -07:00
zomars 31f2002654 Update lockfile.yml 2023-04-11 11:38:20 -07:00
zomars@users.noreply.github.com ee155c3643 Update yarn.lock 2023-04-11 18:23:33 +00:00
zomars 8ab20c3c9f Update lockfile.yml 2023-04-11 11:21:17 -07:00
zomars 4f6d035d29 Create lockfile.yml 2023-04-11 11:07:55 -07:00
zomars 2e5b42ef47 Update yarn.lock 2023-04-11 10:56:30 -07:00
Alex van Andel b055672ea0 v2.7.13
release.yml / release (push) Canceled after 0s
2023-04-11 19:51:38 +02:00
Alex van Andel e4fbca388c Fix yarn.lock file 2023-04-11 19:50:36 +02:00
Joe Au-Yeung 2b8bba09c9 Prevent updating app to enabled on seed app store (#8092) 2023-04-11 21:51:21 +05:30
Alex van Andel 98cf29f655 Wrap useSuspense with NoSSR (#8079) 2023-04-11 21:49:27 +05:30
GitStart-Cal.com ae8706316f CALCOM-7895 - [CAL-1320] remove "application_fee_amount" for all regions (#7933) 2023-04-11 16:16:43 +00:00
Jeroen ReumkensandAlex van Andel ec8e08d442 Added serverside translations to teams page. (#7999)
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-04-11 21:39:02 +05:30
sean-brydonandPeer Richelsen c33f424333 Fixes /more page + fixes multiple duration badge on BookingPage (#8200)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-04-11 15:21:24 +00:00
Hariom Balhara 6f2312d1e2 Fix app not visible in app-store (#8202) 2023-04-11 14:58:57 +00:00
137 changed files with 3119 additions and 785 deletions
+6 -3
View File
@@ -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>
);
};
+54 -12
View File
@@ -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")} />}
</>
)}
/>
+17 -7
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "2.7.12",
"version": "2.7.15",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
+46 -35
View File
@@ -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,
+3 -3
View File
@@ -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">
+10
View File
@@ -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"
}
+37 -15
View File
@@ -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é"
}
-2
View File
@@ -44,8 +44,6 @@
--cal-brand-text: white;
}
.dark {
color-scheme: dark;
/* background */
--cal-bg-emphasis: #2b2b2b;
+1
View File
@@ -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") };
+2 -1
View File
@@ -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";
+1 -18
View File
@@ -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,
+2 -2
View File
@@ -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,
+1 -1
View File
@@ -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"
}
Binary file not shown.

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";
+7
View File
@@ -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.
+26
View File
@@ -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": {
Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

+13
View File
@@ -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(),
+12 -1
View File
@@ -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.
+1 -1
View File
@@ -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",
Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

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";
+1 -1
View File
@@ -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";
+20
View File
@@ -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;
+5
View File
@@ -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}
/>
);
};
+1
View File
@@ -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"),
};
}
}
+16 -3
View File
@@ -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}
/>
</>
);

Some files were not shown because too many files have changed in this diff Show More