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:
parent
dddb494071
commit
98829d23d3
12 changed files with 179 additions and 118 deletions
|
@ -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
|
||||
'agree'.
|
||||
<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
|
||||
'agree'.
|
||||
</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 'agree'.
|
||||
<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 '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>
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
@ -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">
|
||||
​
|
||||
</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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.",
|
||||
|
|
Loading…
Reference in a new issue