fix: type errors and translate ee pages (#1050)

* fix: type errors and translate ee pages

* fix: translation key for composed string

* type fixes

Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
This commit is contained in:
Mihai C 2021-10-29 01:58:26 +03:00 committed by GitHub
parent dddb494071
commit 98829d23d3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 179 additions and 118 deletions

View file

@ -1,12 +1,16 @@
import { XIcon } from "@heroicons/react/outline";
import { BadgeCheckIcon } from "@heroicons/react/solid";
import { Trans } from "react-i18next";
import { useLocale } from "@lib/hooks/useLocale";
import { Dialog, DialogTrigger } from "@components/Dialog";
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
export default function LicenseBanner() {
const { t } = useLocale();
/*
Set this value to 'agree' to accept our license:
Set this value to 'agree' to accept our license:
LICENSE: https://github.com/calendso/calendso/blob/main/LICENSE
Summary of terms:
@ -30,9 +34,11 @@ export default function LicenseBanner() {
</span>
<p className="ml-3 font-medium text-white truncate">
<span className="inline">
Accept our license by changing the .env variable{" "}
<span className="bg-gray-50 bg-opacity-20 px-1">NEXT_PUBLIC_LICENSE_CONSENT</span> to
&apos;agree&apos;.
<Trans i18nKey="accept_our_license" values={{ agree: "agree" }}>
Accept our license by changing the .env variable{" "}
<span className="bg-gray-50 bg-opacity-20 px-1">NEXT_PUBLIC_LICENSE_CONSENT</span> to
&apos;agree&apos;.
</Trans>
</span>
</p>
</div>
@ -40,7 +46,7 @@ export default function LicenseBanner() {
<Dialog>
<DialogTrigger asChild>
<button className="rounded-sm w-full flex items-center justify-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium text-green-600 bg-white hover:bg-green-50">
Accept License
{t("accept_license")}
</button>
</DialogTrigger>
<DialogContent />
@ -50,7 +56,7 @@ export default function LicenseBanner() {
<Dialog>
<DialogTrigger asChild>
<button className="-mr-1 flex p-2 rounded-sm hover:bg-green-500 focus:outline-none focus:ring-2 focus:ring-white">
<span className="sr-only">Dismiss</span>
<span className="sr-only">{t("dismiss")}</span>
<XIcon className="h-6 w-6 text-white" aria-hidden="true" />
</button>
</DialogTrigger>
@ -67,18 +73,22 @@ export default function LicenseBanner() {
return (
<ConfirmationDialogContent
variety="success"
title="Open .env and agree to our License"
confirmBtnText="I've changed my .env"
cancelBtnText="Cancel">
To remove this banner, please open your .env file and change the{" "}
<span className="bg-green-400 text-green-500 bg-opacity-20 p-[2px]">NEXT_PUBLIC_LICENSE_CONSENT</span>{" "}
variable to &apos;agree&apos;.
<h2 className="mt-8 mb-2 text-black font-cal">Summary of terms:</h2>
title={t("open_env")}
confirmBtnText={t("env_changed")}
cancelBtnText={t("cancel")}>
<Trans i18nKey="remove_banner_instructions" values={{ agree: "agree" }}>
To remove this banner, please open your .env file and change the{" "}
<span className="bg-green-400 text-green-500 bg-opacity-20 p-[2px]">
NEXT_PUBLIC_LICENSE_CONSENT
</span>{" "}
variable to &apos;agreeapos;.
</Trans>
<h2 className="mt-8 mb-2 text-black font-cal">{t("terms_summary")}:</h2>
<ul className="ml-5 list-disc">
<li>The codebase has to stay open source, whether it was modified or not</li>
<li>You can not repackage or sell the codebase</li>
<li>{t("codebase_has_to_stay_opensource")}</li>
<li>{t("cannot_repackage_codebase")}</li>
<li>
Acquire a commercial license to remove these terms by emailing:{" "}
{t("acquire_license")}:{" "}
<a className="text-blue-500 underline" href="mailto:license@cal.com">
license@cal.com
</a>

View file

@ -1,11 +1,14 @@
import { CardElement, useStripe, useElements } from "@stripe/react-stripe-js";
import { StripeCardElementChangeEvent } from "@stripe/stripe-js";
import { useRouter } from "next/router";
import { stringify } from "querystring";
import React, { useState } from "react";
import { SyntheticEvent } from "react";
import { PaymentData } from "@ee/lib/stripe/server";
import useDarkMode from "@lib/core/browser/useDarkMode";
import { useLocale } from "@lib/hooks/useLocale";
import Button from "@components/ui/Button";
@ -34,6 +37,7 @@ type Props = {
};
eventType: { id: number };
user: { username: string | null };
location: string;
};
type States =
@ -43,6 +47,7 @@ type States =
| { status: "ok" };
export default function PaymentComponent(props: Props) {
const { t } = useLocale();
const router = useRouter();
const { name, date } = router.query;
const [state, setState] = useState<States>({ status: "idle" });
@ -56,15 +61,17 @@ export default function PaymentComponent(props: Props) {
CARD_OPTIONS.style.base["::placeholder"].color = "#fff";
}
const handleChange = async (event) => {
const handleChange = async (event: StripeCardElementChangeEvent) => {
// Listen for changes in the CardElement
// and display any errors as the customer types their card details
setState({ status: "idle" });
if (event.emtpy || event.error)
setState({ status: "error", error: new Error(event.error?.message || "Missing card fields") });
if (event.error)
setState({ status: "error", error: new Error(event.error?.message || t("missing_card_fields")) });
};
const handleSubmit = async (ev) => {
const handleSubmit = async (ev: SyntheticEvent) => {
ev.preventDefault();
if (!stripe || !elements) return;
const card = elements.getElement(CardElement);
if (!card) return;
@ -87,11 +94,11 @@ export default function PaymentComponent(props: Props) {
name,
};
if (payload["location"]) {
if (payload["location"].includes("integration")) {
params.location = "Web conferencing details to follow.";
if (props.location) {
if (props.location.includes("integration")) {
params.location = t("web_conferencing_details_to_follow");
} else {
params.location = payload["location"];
params.location = props.location;
}
}
@ -104,19 +111,19 @@ export default function PaymentComponent(props: Props) {
return (
<form id="payment-form" className="mt-4" onSubmit={handleSubmit}>
<CardElement id="card-element" options={CARD_OPTIONS} onChange={handleChange} />
<div className="flex mt-2 justify-center">
<div className="flex justify-center mt-2">
<Button
type="submit"
disabled={["processing", "error"].includes(state.status)}
loading={state.status === "processing"}
id="submit">
<span id="button-text">
{state.status === "processing" ? <div className="spinner" id="spinner" /> : "Pay now"}
{state.status === "processing" ? <div className="spinner" id="spinner" /> : t("pay_now")}
</span>
</Button>
</div>
{state.status === "error" && (
<div className="mt-4 text-gray-700 dark:text-gray-300 text-center" role="alert">
<div className="mt-4 text-center text-gray-700 dark:text-gray-300" role="alert">
{state.error.message}
</div>
)}

View file

@ -12,6 +12,7 @@ import PaymentComponent from "@ee/components/stripe/Payment";
import getStripe from "@ee/lib/stripe/client";
import { PaymentPageProps } from "@ee/pages/payment/[uid]";
import { useLocale } from "@lib/hooks/useLocale";
import useTheme from "@lib/hooks/useTheme";
dayjs.extend(utc);
@ -19,6 +20,7 @@ dayjs.extend(toArray);
dayjs.extend(timezone);
const PaymentPage: FC<PaymentPageProps> = (props) => {
const { t } = useLocale();
const [is24h, setIs24h] = useState(false);
const [date, setDate] = useState(dayjs.utc(props.booking.startTime));
const { isReady } = useTheme(props.profile.theme);
@ -31,43 +33,45 @@ const PaymentPage: FC<PaymentPageProps> = (props) => {
const eventName = props.booking.title;
return isReady ? (
<div className="bg-neutral-50 dark:bg-neutral-900 h-screen">
<div className="h-screen bg-neutral-50 dark:bg-neutral-900">
<Head>
<title>Payment | {eventName} | Calendso</title>
<title>
{t("payment")} | {eventName} | Cal.com
</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<main className="max-w-3xl mx-auto py-24">
<div className="fixed z-50 inset-0 overflow-y-auto">
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div className="fixed inset-0 my-4 sm:my-0 transition-opacity" aria-hidden="true">
<main className="max-w-3xl py-24 mx-auto">
<div className="fixed inset-0 z-50 overflow-y-auto">
<div className="flex items-end justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
<div className="fixed inset-0 my-4 transition-opacity sm:my-0" aria-hidden="true">
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
&#8203;
</span>
<div
className="inline-block align-bottom dark:bg-gray-800 bg-white rounded-sm px-8 pt-5 pb-4 text-left overflow-hidden border border-neutral-200 dark:border-neutral-700 transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:py-6"
className="inline-block px-8 pt-5 pb-4 overflow-hidden text-left align-bottom transition-all transform bg-white border rounded-sm dark:bg-gray-800 border-neutral-200 dark:border-neutral-700 sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:py-6"
role="dialog"
aria-modal="true"
aria-labelledby="modal-headline">
<div>
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100">
<CreditCardIcon className="h-8 w-8 text-green-600" />
<div className="flex items-center justify-center w-12 h-12 mx-auto bg-green-100 rounded-full">
<CreditCardIcon className="w-8 h-8 text-green-600" />
</div>
<div className="mt-3 text-center sm:mt-5">
<h3
className="text-2xl leading-6 font-semibold dark:text-white text-neutral-900"
className="text-2xl font-semibold leading-6 dark:text-white text-neutral-900"
id="modal-headline">
Payment
{t("payment")}
</h3>
<div className="mt-3">
<p className="text-sm text-neutral-600 dark:text-gray-300">
You have also received an email with this link, if you want to pay later.
{t("pay_later_instructions")}
</p>
</div>
<div className="mt-4 text-gray-700 dark:text-gray-300 border-t border-b dark:border-gray-900 py-4 grid grid-cols-3 text-left">
<div className="font-medium">What</div>
<div className="mb-6 col-span-2">{eventName}</div>
<div className="font-medium">When</div>
<div className="mb-6 col-span-2">
<div className="grid grid-cols-3 py-4 mt-4 text-left text-gray-700 border-t border-b dark:text-gray-300 dark:border-gray-900">
<div className="font-medium">{t("what")}</div>
<div className="col-span-2 mb-6">{eventName}</div>
<div className="font-medium">{t("when")}</div>
<div className="col-span-2 mb-6">
{date.format("dddd, DD MMMM YYYY")}
<br />
{date.format(is24h ? "H:mm" : "h:mma")} - {props.eventType.length} mins{" "}
@ -77,12 +81,12 @@ const PaymentPage: FC<PaymentPageProps> = (props) => {
</div>
{props.booking.location && (
<>
<div className="font-medium">Where</div>
<div className="mb-6 col-span-2">{props.booking.location}</div>
<div className="font-medium">{t("where")}</div>
<div className="col-span-2 mb-6">{props.booking.location}</div>
</>
)}
<div className="font-medium">Price</div>
<div className="mb-6 col-span-2">
<div className="font-medium">{t("price")}</div>
<div className="col-span-2 mb-6">
<IntlProvider locale="en">
<FormattedNumber
value={props.eventType.price / 100.0}
@ -96,7 +100,7 @@ const PaymentPage: FC<PaymentPageProps> = (props) => {
</div>
<div>
{props.payment.success && !props.payment.refunded && (
<div className="mt-4 text-gray-700 dark:text-gray-300 text-center">Paid</div>
<div className="mt-4 text-center text-gray-700 dark:text-gray-300">{t("paid")}</div>
)}
{!props.payment.success && (
<Elements stripe={getStripe(props.payment.data.stripe_publishable_key)}>
@ -104,16 +108,17 @@ const PaymentPage: FC<PaymentPageProps> = (props) => {
payment={props.payment}
eventType={props.eventType}
user={props.user}
location={props.booking.location}
/>
</Elements>
)}
{props.payment.refunded && (
<div className="mt-4 text-gray-700 dark:text-gray-300 text-center">Refunded</div>
<div className="mt-4 text-center text-gray-700 dark:text-gray-300">{t("refunded")}</div>
)}
</div>
{!props.profile.hideBranding && (
<div className="mt-4 pt-4 border-t dark:border-gray-900 text-gray-400 text-center text-xs dark:text-white">
<a href="https://cal.com/signup">Create your own booking link with Cal.com</a>
<div className="pt-4 mt-4 text-xs text-center text-gray-400 border-t dark:border-gray-900 dark:text-white">
<a href="https://cal.com/signup">{t("create_booking_link_with_calcom")}</a>
</div>
)}
</div>

View file

@ -2,10 +2,12 @@ import { ChatAltIcon } from "@heroicons/react/solid";
import { useIntercom } from "react-use-intercom";
import classNames from "@lib/classNames";
import { useLocale } from "@lib/hooks/useLocale";
import { DropdownMenuItem } from "@components/ui/Dropdown";
const HelpMenuItem = () => {
const { t } = useLocale();
const { boot, show } = useIntercom();
return (
<DropdownMenuItem>
@ -22,7 +24,7 @@ const HelpMenuItem = () => {
)}
aria-hidden="true"
/>
Help
{t("help")}
</button>
</DropdownMenuItem>
);

View file

@ -6,6 +6,7 @@ import { v4 as uuidv4 } from "uuid";
import { CalendarEvent } from "@lib/calendarClient";
import EventOrganizerRefundFailedMail from "@lib/emails/EventOrganizerRefundFailedMail";
import EventPaymentMail from "@lib/emails/EventPaymentMail";
import { getErrorFromUnknown } from "@lib/errors";
import prisma from "@lib/prisma";
import { createPaymentLink } from "./client";
@ -79,8 +80,7 @@ export async function handlePayment(
name: booking.user?.name,
date: booking.startTime.toISOString(),
}),
evt,
booking.uid
evt
);
await mail.sendEmail();
@ -110,7 +110,6 @@ export async function refund(
if (payment.type != PaymentType.STRIPE) {
await handleRefundError({
event: calEvent,
booking: booking,
reason: "cannot refund non Stripe payment",
paymentId: "unknown",
});
@ -127,7 +126,6 @@ export async function refund(
if (!refund || refund.status === "failed") {
await handleRefundError({
event: calEvent,
booking: booking,
reason: refund?.failure_reason || "unknown",
paymentId: payment.externalId,
});
@ -143,30 +141,20 @@ export async function refund(
},
});
} catch (e) {
console.error(e, "Refund failed");
const err = getErrorFromUnknown(e);
console.error(err, "Refund failed");
await handleRefundError({
event: calEvent,
booking: booking,
reason: e.message || "unknown",
reason: err.message || "unknown",
paymentId: "unknown",
});
}
}
async function handleRefundError(opts: {
event: CalendarEvent;
booking: { id: number; uid: string };
reason: string;
paymentId: string;
}) {
console.error(`refund failed: ${opts.reason} for booking '${opts.booking.id}'`);
async function handleRefundError(opts: { event: CalendarEvent; reason: string; paymentId: string }) {
console.error(`refund failed: ${opts.reason} for booking '${opts.event.uid}'`);
try {
await new EventOrganizerRefundFailedMail(
opts.event,
opts.booking.uid,
opts.reason,
opts.paymentId
).sendEmail();
await new EventOrganizerRefundFailedMail(opts.event, opts.reason, opts.paymentId).sendEmail();
} catch (e) {
console.error("Error while sending refund error email", e);
}

View file

@ -55,6 +55,7 @@ async function handlePaymentSuccess(event: Stripe.Event) {
timeZone: true,
email: true,
name: true,
locale: true,
},
},
},
@ -72,7 +73,7 @@ async function handlePaymentSuccess(event: Stripe.Event) {
if (!user) throw new Error("No user found");
const t = await getTranslation(/* FIXME handle mulitple locales here */ "en", "common");
const t = await getTranslation(user.locale ?? "en", "common");
const evt: Ensure<CalendarEvent, "language"> = {
type: booking.title,
@ -85,6 +86,7 @@ async function handlePaymentSuccess(event: Stripe.Event) {
uid: booking.uid,
language: t,
};
if (booking.location) evt.location = booking.location;
if (booking.confirmed) {

View file

@ -16,23 +16,27 @@ export default class EventOrganizerRefundFailedMail extends EventOrganizerMail {
reason: string;
paymentId: string;
constructor(calEvent: CalendarEvent, uid: string, reason: string, paymentId: string) {
super(calEvent, uid, undefined);
constructor(calEvent: CalendarEvent, reason: string, paymentId: string) {
super(calEvent);
this.reason = reason;
this.paymentId = paymentId;
}
protected getBodyHeader(): string {
return "A refund failed";
return this.calEvent.language("a_refund_failed");
}
protected getBodyText(): string {
const organizerStart: Dayjs = dayjs(this.calEvent.startTime).tz(this.calEvent.organizer.timeZone);
return `The refund for the event ${this.calEvent.type} with ${
this.calEvent.attendees[0].name
} on ${organizerStart.format("LT dddd, LL")} failed. Please check with your payment provider and ${
this.calEvent.attendees[0].name
} how to handle this.<br>The error message was: '${this.reason}'<br>PaymentId: '${this.paymentId}'`;
return `${this.calEvent.language("refund_failed", {
eventType: this.calEvent.type,
userName: this.calEvent.attendees[0].name,
date: organizerStart.format("LT dddd, LL"),
})} ${this.calEvent.language("check_with_provider_and_user", {
userName: this.calEvent.attendees[0].name,
})}<br>${this.calEvent.language("error_message", { errorMessage: this.reason })}<br>PaymentId: '${
this.paymentId
}'`;
}
protected getAdditionalBody(): string {
@ -58,8 +62,10 @@ export default class EventOrganizerRefundFailedMail extends EventOrganizerMail {
protected getSubject(): string {
const organizerStart: Dayjs = dayjs(this.calEvent.startTime).tz(this.calEvent.organizer.timeZone);
return `Refund failed: ${this.calEvent.attendees[0].name} - ${organizerStart.format("LT dddd, LL")} - ${
this.calEvent.type
}`;
return this.calEvent.language("refund_failed_subject", {
userName: this.calEvent.attendees[0].name,
date: organizerStart.format("LT dddd, LL"),
eventType: this.calEvent.type,
});
}
}

View file

@ -13,13 +13,15 @@ dayjs.extend(localizedFormat);
export default class EventOrganizerRequestReminderMail extends EventOrganizerRequestMail {
protected getBodyHeader(): string {
return "An event is still waiting for your approval.";
return this.calEvent.language("still_waiting_for_approval");
}
protected getSubject(): string {
const organizerStart: Dayjs = dayjs(this.calEvent.startTime).tz(this.calEvent.organizer.timeZone);
return `Event request is still waiting: ${this.calEvent.attendees[0].name} - ${organizerStart.format(
"LT dddd, LL"
)} - ${this.calEvent.type}`;
return this.calEvent.language("event_is_still_waiting", {
attendeeName: this.calEvent.attendees[0].name,
date: organizerStart.format("LT dddd, LL"),
eventType: this.calEvent.type,
});
}
}

View file

@ -5,7 +5,7 @@ import utc from "dayjs/plugin/utc";
import { CalendarEvent } from "@lib/calendarClient";
import EventMail, { AdditionInformation } from "./EventMail";
import EventMail from "./EventMail";
dayjs.extend(utc);
dayjs.extend(timezone);
@ -14,13 +14,8 @@ dayjs.extend(localizedFormat);
export default class EventPaymentMail extends EventMail {
paymentLink: string;
constructor(
paymentLink: string,
calEvent: CalendarEvent,
uid: string,
additionInformation: AdditionInformation = null
) {
super(calEvent, uid, additionInformation);
constructor(paymentLink: string, calEvent: CalendarEvent) {
super(calEvent);
this.paymentLink = paymentLink;
}
@ -59,8 +54,10 @@ export default class EventPaymentMail extends EventMail {
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<h1 style="font-weight: 500; color: #161e2e;">Your meeting is awaiting payment</h1>
<p style="color: #4b5563; margin-bottom: 30px;">You and any other attendees have been emailed with this information.</p>
<h1 style="font-weight: 500; color: #161e2e;">${this.calEvent.language("meeting_awaiting_payment")}</h1>
<p style="color: #4b5563; margin-bottom: 30px;">${this.calEvent.language(
"emailed_you_and_any_other_attendees"
)}</p>
<hr />
<table style="border-spacing: 20px; color: #161e2e; margin-bottom: 10px;">
<colgroup>
@ -68,25 +65,25 @@ export default class EventPaymentMail extends EventMail {
<col span="1" style="width: 60%;">
</colgroup>
<tr>
<td>What</td>
<td>${this.calEvent.language("what")}</td>
<td>${this.calEvent.type}</td>
</tr>
<tr>
<td>When</td>
<td>${this.calEvent.language("when")}</td>
<td>${this.getInviteeStart().format("dddd, LL")}<br>${this.getInviteeStart().format("h:mma")} (${
this.calEvent.attendees[0].timeZone
})</td>
</tr>
<tr>
<td>Who</td>
<td>${this.calEvent.language("who")}</td>
<td>${this.calEvent.organizer.name}<br /><small>${this.calEvent.organizer.email}</small></td>
</tr>
<tr>
<td>Where</td>
<td>${this.calEvent.language("where")}</td>
<td>${this.getLocation()}</td>
</tr>
<tr>
<td>Notes</td>
<td>${this.calEvent.language("notes")}Notes</td>
<td>${this.calEvent.description}</td>
</tr>
</table>
@ -109,15 +106,18 @@ export default class EventPaymentMail extends EventMail {
* @protected
*/
protected getLocation(): string {
if (this.additionInformation?.hangoutLink) {
return `<a href="${this.additionInformation?.hangoutLink}">${this.additionInformation?.hangoutLink}</a><br />`;
if (this.calEvent.additionInformation?.hangoutLink) {
return `<a href="${this.calEvent.additionInformation?.hangoutLink}">${this.calEvent.additionInformation?.hangoutLink}</a><br />`;
}
if (this.additionInformation?.entryPoints && this.additionInformation?.entryPoints.length > 0) {
const locations = this.additionInformation?.entryPoints
if (
this.calEvent.additionInformation?.entryPoints &&
this.calEvent.additionInformation?.entryPoints.length > 0
) {
const locations = this.calEvent.additionInformation?.entryPoints
.map((entryPoint) => {
return `
Join by ${entryPoint.entryPointType}: <br />
${this.calEvent.language("join_by_entrypoint", { entryPoint: entryPoint.entryPointType })}: <br />
<a href="${entryPoint.uri}">${entryPoint.label}</a> <br />
`;
})
@ -130,7 +130,7 @@ export default class EventPaymentMail extends EventMail {
}
protected getAdditionalBody(): string {
return `<a href="${this.paymentLink}">Pay now</a>`;
return `<a href="${this.paymentLink}">${this.calEvent.language("pay_now")}</a>`;
}
/**
@ -143,15 +143,17 @@ export default class EventPaymentMail extends EventMail {
to: `${this.calEvent.attendees[0].name} <${this.calEvent.attendees[0].email}>`,
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
replyTo: this.calEvent.organizer.email,
subject: `Awaiting Payment: ${this.calEvent.type} with ${
this.calEvent.organizer.name
} on ${this.getInviteeStart().format("dddd, LL")}`,
subject: this.calEvent.language("awaiting_payment", {
eventType: this.calEvent.type,
organizerName: this.calEvent.organizer.name,
date: this.getInviteeStart().format("dddd, LL"),
}),
html: this.getHtmlRepresentation(),
text: this.getPlainTextRepresentation(),
};
}
protected printNodeMailerError(error: string): void {
protected printNodeMailerError(error: Error): void {
console.error("SEND_BOOKING_PAYMENT_ERROR", this.calEvent.attendees[0].email, error);
}

View file

@ -1,8 +1,9 @@
import nodemailer, { SentMessageInfo } from "nodemailer";
import { SendMailOptions } from "nodemailer";
import { serverConfig } from "../serverConfig";
const sendEmail = ({ to, subject, text, html = null }): Promise<string | SentMessageInfo> =>
const sendEmail = ({ to, subject, text, html }: SendMailOptions): Promise<string | SentMessageInfo> =>
new Promise((resolve, reject) => {
const { transport, from } = serverConfig;

View file

@ -37,7 +37,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
startTime: true,
endTime: true,
attendees: true,
user: true,
user: {
select: {
email: true,
name: true,
username: true,
locale: true,
timeZone: true,
},
},
id: true,
uid: true,
},
@ -62,7 +70,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
console.error(`Booking ${booking.id} is missing required properties for booking reminder`, { user });
continue;
}
const t = await getTranslation(req.body.language ?? "en", "common");
const t = await getTranslation(user.locale ?? "en", "common");
const evt: CalendarEvent = {
type: booking.title,
title: booking.title,

View file

@ -1,4 +1,30 @@
{
"accept_our_license": "Accept our license by changing the .env variable <1>NEXT_PUBLIC_LICENSE_CONSENT</1> to '{{agree}}'.",
"remove_banner_instructions": "To remove this banner, please open your .env file and change the <1>NEXT_PUBLIC_LICENSE_CONSENT</1> variable to '{{agree}}'.",
"error_message": "The error message was: '{{errorMessage}}'",
"refund_failed_subject": "Refund failed: {{userName}} - {{date}} - {{eventType}}",
"refund_failed": "The refund for the event {{eventType}} with {{userName}} on {{date}} failed.",
"check_with_provider_and_user": "Please check with your payment provider and {{userName}} how to handle this.",
"a_refund_failed": "A refund failed",
"awaiting_payment": "Awaiting Payment: {{eventType}} with {{organizerName}} on {{date}}",
"meeting_awaiting_payment": "Your meeting is awaiting payment",
"help": "Help",
"price": "Price",
"paid": "Paid",
"refunded": "Refunded",
"pay_later_instructions": "You have also received an email with this link, if you want to pay later.",
"payment": "Payment",
"missing_card_fields": "Missing card fields",
"pay_now": "Pay now",
"codebase_has_to_stay_opensource": "The codebase has to stay open source, whether it was modified or not",
"cannot_repackage_codebase": "You can not repackage or sell the codebase",
"acquire_license": "Acquire a commercial license to remove these terms by emailing",
"terms_summary": "Summary of terms",
"open_env": "Open .env and agree to our License",
"env_changed": "I've changed my .env",
"accept_license": "Accept License",
"still_waiting_for_approval": "An event is still waiting for approval",
"event_is_still_waiting": "Event request is still waiting: {{attendeeName}} - {{date}} - {{eventType}}",
"no_more_results": "No more results",
"load_more_results": "Load more results",
"integration_meeting_id": "{{integrationName}} meeting ID: {{meetingId}}",
@ -159,6 +185,7 @@
"add_to_calendar": "Add to calendar",
"other": "Other",
"emailed_you_and_attendees": "We emailed you and the other attendees a calendar invitation with all the details.",
"emailed_you_and_any_other_attendees": "You and any other attendees have been emailed with this information.",
"needs_to_be_confirmed_or_rejected": "Your booking still needs to be confirmed or rejected.",
"user_needs_to_confirm_or_reject_booking": "{{user}} still needs to confirm or reject the booking.",
"meeting_is_scheduled": "This meeting is scheduled",
@ -459,7 +486,6 @@
"date_range": "Date Range",
"calendar_days": "calendar days",
"business_days": "business days",
"payment": "Payment",
"set_address_place": "Set an address or place",
"cal_invitee_phone_number_scheduling": "Cal will ask your invitee to enter a phone number before scheduling.",
"cal_provide_google_meet_location": "Cal will provide a Google Meet location.",