Compare commits
16 Commits
main
...
feat/add_w
Author | SHA1 | Date |
---|---|---|
Edward Fernandez | 20beb961c6 | |
Edward Fernandez | 036224f7bd | |
kodiakhq[bot] | cf8f43ee6d | |
Peer Richelsen | 1f65bc77f5 | |
Yuval Drori | 38886e81ca | |
kodiakhq[bot] | cb2842c695 | |
kodiakhq[bot] | 7f2323a0d6 | |
Yuval Drori | 12c728b4e4 | |
kodiakhq[bot] | a5e37fe8b6 | |
Yuval Drori | 9ed4295b28 | |
Yuval Drori | cf75791823 | |
Omar López | 2ada162c90 | |
Peer Richelsen | f670cc23f2 | |
Peer Richelsen | 59720b8ca7 | |
Peer Richelsen | 3847196aa6 | |
Yuval Drori | 45eb4422e3 |
|
@ -1,4 +1,4 @@
|
|||
# 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:
|
||||
|
@ -70,3 +70,6 @@ CALENDSO_ENCRYPTION_KEY=
|
|||
|
||||
# Intercom Config
|
||||
NEXT_PUBLIC_INTERCOM_APP_ID=
|
||||
|
||||
# Web3/Crypto stuff
|
||||
NEXT_PUBLIC_BLOXY_API_KEY=
|
||||
|
|
|
@ -0,0 +1,129 @@
|
|||
import { useCallback, useMemo, useState } from "react";
|
||||
import Web3 from "web3";
|
||||
import { AbiItem } from "web3-utils";
|
||||
|
||||
import showToast from "@lib/notification";
|
||||
|
||||
import { Button } from "@components/ui/Button";
|
||||
|
||||
import genericAbi from "../web3/abis/abiWithGetBalance.json";
|
||||
|
||||
interface Window {
|
||||
ethereum: any;
|
||||
web3: Web3;
|
||||
}
|
||||
|
||||
interface EvtsToVerify {
|
||||
[eventId: string]: boolean;
|
||||
}
|
||||
|
||||
declare const window: Window;
|
||||
|
||||
interface CryptoSectionProps {
|
||||
id: number | string;
|
||||
smartContractAddress: string;
|
||||
/** When set to true, there will be only 1 button which will both connect Metamask and verify the user's wallet. Otherwise, it will be in 2 steps with 2 buttons. */
|
||||
oneStep: boolean;
|
||||
verified: boolean | undefined;
|
||||
setEvtsToVerify: React.Dispatch<React.SetStateAction<Record<number | string, boolean>>>;
|
||||
}
|
||||
|
||||
const CryptoSection = (props: CryptoSectionProps) => {
|
||||
// Crypto section which should be shown on booking page if event type requires a smart contract token.
|
||||
const [ethEnabled, toggleEthEnabled] = useState<boolean>(false);
|
||||
|
||||
const connectMetamask = useCallback(async () => {
|
||||
if (window.ethereum) {
|
||||
await window.ethereum.request({ method: "eth_requestAccounts" });
|
||||
window.web3 = new Web3(window.ethereum);
|
||||
toggleEthEnabled(true);
|
||||
} else {
|
||||
toggleEthEnabled(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const verifyWallet = useCallback(async () => {
|
||||
try {
|
||||
const contract = new window.web3.eth.Contract(genericAbi as AbiItem[], props.smartContractAddress);
|
||||
const balance = await contract.methods.balanceOf(window.ethereum.selectedAddress).call();
|
||||
|
||||
const hasToken = balance > 0;
|
||||
|
||||
props.setEvtsToVerify((prevState: EvtsToVerify) => {
|
||||
const changedEvt = { [props.id]: hasToken };
|
||||
return { ...prevState, ...changedEvt };
|
||||
});
|
||||
|
||||
if (!hasToken)
|
||||
throw new Error("Specified wallet does not own any tokens belonging to this smart contract");
|
||||
} catch (err) {
|
||||
err instanceof Error ? showToast(err.message, "error") : showToast("An error has occurred", "error");
|
||||
}
|
||||
}, [props]);
|
||||
|
||||
// @TODO: Show error on either of buttons if fails. Yup schema already contains the error message.
|
||||
const successButton = useMemo(() => {
|
||||
return (
|
||||
<Button type="button" disabled>
|
||||
Success!
|
||||
</Button>
|
||||
);
|
||||
}, []);
|
||||
|
||||
const verifyButton = useMemo(() => {
|
||||
return (
|
||||
<Button onClick={verifyWallet} type="button" id="hasToken" name="hasToken">
|
||||
Verify wallet
|
||||
</Button>
|
||||
);
|
||||
}, [verifyWallet]);
|
||||
|
||||
const connectButton = useMemo(() => {
|
||||
return (
|
||||
<Button onClick={connectMetamask} type="button">
|
||||
Connect Metamask
|
||||
</Button>
|
||||
);
|
||||
}, [connectMetamask]);
|
||||
|
||||
const oneStepButton = useMemo(() => {
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
await connectMetamask();
|
||||
await verifyWallet();
|
||||
}}>
|
||||
Verify wallet
|
||||
</Button>
|
||||
);
|
||||
}, [connectMetamask, verifyWallet]);
|
||||
|
||||
const determineButton = useCallback(() => {
|
||||
// Did it in an extra function for some added readability, but this can be done in a ternary depending on preference
|
||||
if (props.oneStep) {
|
||||
return props.verified ? successButton : oneStepButton;
|
||||
} else {
|
||||
if (ethEnabled) {
|
||||
return props.verified ? successButton : verifyButton;
|
||||
} else {
|
||||
return connectButton;
|
||||
}
|
||||
}
|
||||
}, [props.verified, successButton, oneStepButton, connectButton, ethEnabled, props.oneStep, verifyButton]);
|
||||
|
||||
return (
|
||||
<div
|
||||
id={`crypto-${props.id}`}
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
padding: "2.5%",
|
||||
}}>
|
||||
{determineButton()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CryptoSection;
|
|
@ -9,7 +9,7 @@ import { EventTypeCustomInputType } from "@prisma/client";
|
|||
import dayjs from "dayjs";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
||||
import { FormattedNumber, IntlProvider } from "react-intl";
|
||||
import { ReactMultiEmail } from "react-multi-email";
|
||||
|
@ -39,9 +39,22 @@ import { TeamBookingPageProps } from "../../../pages/team/[slug]/book";
|
|||
|
||||
type BookingPageProps = BookPageProps | TeamBookingPageProps;
|
||||
|
||||
type BookingFormValues = {
|
||||
name: string;
|
||||
email: string;
|
||||
notes?: string;
|
||||
locationType?: LocationType;
|
||||
guests?: string[];
|
||||
phone?: string;
|
||||
customInputs?: {
|
||||
[key: string]: string;
|
||||
};
|
||||
};
|
||||
|
||||
const BookingPage = (props: BookingPageProps) => {
|
||||
const { t, i18n } = useLocale();
|
||||
const router = useRouter();
|
||||
|
||||
/*
|
||||
* This was too optimistic
|
||||
* I started, then I remembered what a beast book/event.ts is
|
||||
|
@ -52,6 +65,7 @@ const BookingPage = (props: BookingPageProps) => {
|
|||
// go to success page.
|
||||
},
|
||||
});*/
|
||||
|
||||
const mutation = useMutation(createBooking, {
|
||||
onSuccess: async ({ attendees, paymentUid, ...responseData }) => {
|
||||
if (paymentUid) {
|
||||
|
@ -127,18 +141,6 @@ const BookingPage = (props: BookingPageProps) => {
|
|||
[LocationType.Daily]: "Daily.co Video",
|
||||
};
|
||||
|
||||
type BookingFormValues = {
|
||||
name: string;
|
||||
email: string;
|
||||
notes?: string;
|
||||
locationType?: LocationType;
|
||||
guests?: string[];
|
||||
phone?: string;
|
||||
customInputs?: {
|
||||
[key: string]: string;
|
||||
};
|
||||
};
|
||||
|
||||
const bookingForm = useForm<BookingFormValues>({
|
||||
defaultValues: {
|
||||
name: (router.query.name as string) || "",
|
||||
|
@ -294,6 +296,12 @@ const BookingPage = (props: BookingPageProps) => {
|
|||
<CalendarIcon className="inline-block w-4 h-4 mr-1 -mt-1" />
|
||||
{parseDate(date)}
|
||||
</p>
|
||||
{props.eventType.smartContractAddress && (
|
||||
<p className="px-2 py-1 mb-1 -ml-2 text-gray-500">
|
||||
Requires ownership of a token belonging to the following address:{" "}
|
||||
{props.eventType.smartContractAddress}
|
||||
</p>
|
||||
)}
|
||||
<p className="mb-8 text-gray-600 dark:text-white">{props.eventType.description}</p>
|
||||
</div>
|
||||
<div className="sm:w-1/2 sm:pl-8 sm:pr-4">
|
||||
|
|
|
@ -3,6 +3,7 @@ import { EventType, SchedulingType } from "@prisma/client";
|
|||
import { WorkingHours } from "./schedule";
|
||||
|
||||
export type AdvancedOptions = {
|
||||
smartContractAddress?: string;
|
||||
eventName?: string;
|
||||
periodType?: string;
|
||||
periodDays?: number;
|
||||
|
|
|
@ -36,8 +36,10 @@
|
|||
"@daily-co/daily-js": "^0.21.0",
|
||||
"@headlessui/react": "^1.4.2",
|
||||
"@heroicons/react": "^1.0.5",
|
||||
"@hookform/resolvers": "^2.8.3",
|
||||
"@hookform/error-message": "^2.0.0",
|
||||
"@hookform/resolvers": "^2.8.5",
|
||||
"@jitsu/sdk-js": "^2.2.4",
|
||||
"@metamask/providers": "^8.1.1",
|
||||
"@next/bundle-analyzer": "11.1.2",
|
||||
"@prisma/client": "3.0.2",
|
||||
"@radix-ui/react-avatar": "^0.1.0",
|
||||
|
@ -100,6 +102,7 @@
|
|||
"tsdav": "^1.1.5",
|
||||
"tslog": "^3.2.1",
|
||||
"uuid": "^8.3.2",
|
||||
"web3": "^1.6.1",
|
||||
"zod": "^3.8.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -2,10 +2,12 @@ import { ArrowRightIcon } from "@heroicons/react/outline";
|
|||
import { GetServerSidePropsContext } from "next";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
import useTheme from "@lib/hooks/useTheme";
|
||||
import showToast from "@lib/notification";
|
||||
import prisma from "@lib/prisma";
|
||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
||||
|
@ -15,6 +17,12 @@ import Avatar from "@components/ui/Avatar";
|
|||
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
import CryptoSection from "../components/CryptoSection";
|
||||
|
||||
interface EvtsToVerify {
|
||||
[evtId: string]: boolean;
|
||||
}
|
||||
|
||||
export default function User(props: inferSSRProps<typeof getServerSideProps>) {
|
||||
const { isReady } = useTheme(props.user.theme);
|
||||
const { user, eventTypes } = props;
|
||||
|
@ -25,6 +33,8 @@ export default function User(props: inferSSRProps<typeof getServerSideProps>) {
|
|||
|
||||
const nameOrUsername = user.name || user.username || "";
|
||||
|
||||
const [evtsToVerify, setEvtsToVerify] = useState<EvtsToVerify>({});
|
||||
|
||||
return (
|
||||
<>
|
||||
<HeadSeo
|
||||
|
@ -51,6 +61,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps>) {
|
|||
{eventTypes.map((type) => (
|
||||
<div
|
||||
key={type.id}
|
||||
style={{ display: "flex" }}
|
||||
className="relative bg-white border rounded-sm group dark:bg-neutral-900 dark:border-0 dark:hover:border-neutral-600 hover:bg-gray-50 border-neutral-200 hover:border-brand">
|
||||
<ArrowRightIcon className="absolute w-4 h-4 text-black transition-opacity opacity-0 right-3 top-3 dark:text-white group-hover:opacity-100" />
|
||||
<Link
|
||||
|
@ -58,11 +69,32 @@ export default function User(props: inferSSRProps<typeof getServerSideProps>) {
|
|||
pathname: `/${user.username}/${type.slug}`,
|
||||
query,
|
||||
}}>
|
||||
<a className="block px-6 py-4" data-testid="event-type-link">
|
||||
<h2 className="font-semibold text-neutral-900 dark:text-white">{type.title}</h2>
|
||||
<a
|
||||
onClick={(e) => {
|
||||
// If a token is required for this event type, add a click listener that checks whether the user verified their wallet or not
|
||||
if (type.smartContractAddress && !evtsToVerify[type.id]) {
|
||||
e.preventDefault();
|
||||
showToast(
|
||||
"You must verify a wallet with a token belonging to the specified smart contract first",
|
||||
"error"
|
||||
);
|
||||
}
|
||||
}}
|
||||
className="block px-6 py-4"
|
||||
data-testid="event-type-link">
|
||||
<h2 className="font-semibold grow text-neutral-900 dark:text-white">{type.title}</h2>
|
||||
<EventTypeDescription eventType={type} />
|
||||
</a>
|
||||
</Link>
|
||||
{type.smartContractAddress && (
|
||||
<CryptoSection
|
||||
id={type.id}
|
||||
smartContractAddress={type.smartContractAddress}
|
||||
verified={evtsToVerify[type.id]}
|
||||
setEvtsToVerify={setEvtsToVerify}
|
||||
oneStep
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
@ -77,6 +109,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps>) {
|
|||
</div>
|
||||
)}
|
||||
</main>
|
||||
<Toaster position="bottom-right" />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
@ -150,6 +183,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||
schedulingType: true,
|
||||
price: true,
|
||||
currency: true,
|
||||
smartContractAddress: true,
|
||||
},
|
||||
take: user.plan === "FREE" ? 1 : undefined,
|
||||
});
|
||||
|
|
|
@ -44,6 +44,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||
schedulingType: true,
|
||||
minimumBookingNotice: true,
|
||||
timeZone: true,
|
||||
smartContractAddress: true,
|
||||
slotInterval: true,
|
||||
users: {
|
||||
select: {
|
||||
|
|
|
@ -56,6 +56,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||
periodDays: true,
|
||||
periodStartDate: true,
|
||||
periodEndDate: true,
|
||||
smartContractAddress: true,
|
||||
periodCountCalendarDays: true,
|
||||
price: true,
|
||||
currency: true,
|
||||
|
|
|
@ -112,6 +112,16 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
}
|
||||
|
||||
if (req.method === "PATCH" || req.method === "POST") {
|
||||
// Data validation
|
||||
// @TODO: Move to dedicated data validation function when there's more data to validate
|
||||
const { smartContractAddress } = req.body;
|
||||
if (smartContractAddress) {
|
||||
// @TODO: Check address actually exists on mainnet
|
||||
if (smartContractAddress.length !== 42 || smartContractAddress.slice(0, 2) !== "0x")
|
||||
return res.status(422).json({ message: "Invalid smart contract address." });
|
||||
}
|
||||
//
|
||||
|
||||
const data: Prisma.EventTypeCreateInput | Prisma.EventTypeUpdateInput = {
|
||||
title: req.body.title,
|
||||
slug: req.body.slug.trim(),
|
||||
|
@ -135,6 +145,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
slotInterval: req.body.slotInterval,
|
||||
price: req.body.price,
|
||||
currency: req.body.currency,
|
||||
smartContractAddress: req.body.smartContractAddress,
|
||||
};
|
||||
|
||||
if (req.body.schedulingType) {
|
||||
|
|
|
@ -224,6 +224,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
userId: true,
|
||||
price: true,
|
||||
currency: true,
|
||||
smartContractAddress: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -35,11 +35,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
schedulingType: true,
|
||||
slug: true,
|
||||
hidden: true,
|
||||
smartContractAddress: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return res.status(200).json({ message: "Events.", data: user.eventTypes });
|
||||
return res.status(200).json({ message: "Events.", data: user?.eventTypes });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ import prisma from "@lib/prisma";
|
|||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const session = await getSession({ req: req });
|
||||
|
||||
if (!session) {
|
||||
if (!session?.user?.id) {
|
||||
res.status(401).json({ message: "Not authenticated" });
|
||||
return;
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
hidden: true,
|
||||
price: true,
|
||||
currency: true,
|
||||
smartContractAddress: true,
|
||||
users: {
|
||||
select: {
|
||||
id: true,
|
||||
|
@ -109,7 +110,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
}, {} as Record<number, EventTypeGroup["eventTypes"][number]>);
|
||||
const mergedEventTypes = Object.values(eventTypesHashMap).map((et, index) => ({
|
||||
...et,
|
||||
$disabled: user.plan === "FREE" && index > 0,
|
||||
$disabled: user?.plan === "FREE" && index > 0,
|
||||
}));
|
||||
|
||||
return res.status(200).json({ eventTypes: mergedEventTypes });
|
||||
|
|
|
@ -39,6 +39,7 @@ import updateEventType from "@lib/mutations/event-types/update-event-type";
|
|||
import showToast from "@lib/notification";
|
||||
import prisma from "@lib/prisma";
|
||||
import { defaultAvatarSrc } from "@lib/profile";
|
||||
import { trpc } from "@lib/trpc";
|
||||
import { AdvancedOptions, EventTypeInput } from "@lib/types/event-type";
|
||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
import { WorkingHours } from "@lib/types/schedule";
|
||||
|
@ -57,9 +58,22 @@ import { DateRangePicker } from "@components/ui/form/DateRangePicker";
|
|||
import MinutesField from "@components/ui/form/MinutesField";
|
||||
import * as RadioArea from "@components/ui/form/radio-area";
|
||||
|
||||
import bloxyApi from "../../web3/dummyResps/bloxyApi";
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
|
||||
interface Token {
|
||||
name?: string;
|
||||
address: string;
|
||||
symbol: string;
|
||||
}
|
||||
|
||||
interface NFT extends Token {
|
||||
// Some OpenSea NFTs have several contracts
|
||||
contracts: Array<Token>;
|
||||
}
|
||||
|
||||
type OptionTypeBase = {
|
||||
label: string;
|
||||
value: LocationType;
|
||||
|
@ -140,6 +154,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
const [customInputs, setCustomInputs] = useState<EventTypeCustomInput[]>(
|
||||
eventType.customInputs.sort((a, b) => a.id - b.id) || []
|
||||
);
|
||||
const [tokensList, setTokensList] = useState<Array<Token>>([]);
|
||||
|
||||
const periodType =
|
||||
PERIOD_TYPES.find((s) => s.type === eventType.periodType) ||
|
||||
|
@ -148,6 +163,45 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
const [requirePayment, setRequirePayment] = useState(eventType.price > 0);
|
||||
const [advancedSettingsVisible, setAdvancedSettingsVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const { data } = trpc.useQuery(["viewer.userSettings"]);
|
||||
|
||||
if (data?.web3App) {
|
||||
const fetchTokens = async () => {
|
||||
// Get a list of most popular ERC20s and ERC777s, combine them into a single list, set as tokensList
|
||||
try {
|
||||
const erc20sList: Array<Token> =
|
||||
// await axios.get(`https://api.bloxy.info/token/list?key=${process.env.BLOXY_API_KEY}`)
|
||||
// ).data
|
||||
bloxyApi.slice(0, 100).map((erc20: Token) => {
|
||||
const { name, address, symbol } = erc20;
|
||||
return { name, address, symbol };
|
||||
});
|
||||
|
||||
const exodiaList = await (await fetch(`https://exodia.io/api/trending?page=1`)).json();
|
||||
|
||||
const nftsList: Array<Token> = exodiaList.map((nft: NFT) => {
|
||||
const { name, contracts } = nft;
|
||||
if (nft.contracts[0]) {
|
||||
const { address, symbol } = contracts[0];
|
||||
return { name, address, symbol };
|
||||
}
|
||||
});
|
||||
|
||||
const unifiedList: Array<Token> = [...erc20sList, ...nftsList];
|
||||
|
||||
setTokensList(unifiedList);
|
||||
} catch (err) {
|
||||
showToast("Failed to load ERC20s & NFTs list. Please enter an address manually.", "error");
|
||||
}
|
||||
};
|
||||
|
||||
console.log(tokensList); // Just here to make sure it passes the gc hook. Can remove once actual use is made of tokensList.
|
||||
|
||||
fetchTokens();
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedTimeZone(eventType.timeZone || "");
|
||||
}, []);
|
||||
|
@ -256,6 +310,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
const formMethods = useForm<{
|
||||
title: string;
|
||||
eventTitle: string;
|
||||
smartContractAddress: string;
|
||||
slug: string;
|
||||
length: number;
|
||||
description: string;
|
||||
|
@ -465,6 +520,8 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
);
|
||||
};
|
||||
|
||||
const { data } = trpc.useQuery(["viewer.userSettings"]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Shell
|
||||
|
@ -516,6 +573,8 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
advancedPayload.periodEndDate = values.periodDates.endDate || undefined;
|
||||
advancedPayload.minimumBookingNotice = values.minimumBookingNotice;
|
||||
advancedPayload.slotInterval = values.slotInterval;
|
||||
advancedPayload.smartContractAddress = values.smartContractAddress; // TODO @edward: use new `metadata` here to show/hide
|
||||
// prettier-ignore
|
||||
advancedPayload.price = requirePayment
|
||||
? Math.round(parseFloat(asStringOrThrow(values.price)) * 100)
|
||||
: 0;
|
||||
|
@ -722,6 +781,28 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{data?.web3App && (
|
||||
<div className="items-center block sm:flex">
|
||||
<div className="mb-4 min-w-48 sm:mb-0">
|
||||
<label
|
||||
htmlFor="smartContractAddress"
|
||||
className="flex text-sm font-medium text-neutral-700">
|
||||
{t("Smart Contract Address")}
|
||||
</label>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<div className="relative mt-1 rounded-sm shadow-sm">
|
||||
<input
|
||||
type="text"
|
||||
className="block w-full border-gray-300 rounded-sm shadow-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
|
||||
placeholder={t("Example: 0x71c7656ec7ab88b098defb751b7401b5f6d8976f")}
|
||||
defaultValue={eventType.smartContractAddress || ""}
|
||||
{...formMethods.register("smartContractAddress")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="items-center block sm:flex">
|
||||
<div className="mb-4 min-w-48 sm:mb-0">
|
||||
<label
|
||||
|
@ -1367,6 +1448,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||
customInputs: true,
|
||||
timeZone: true,
|
||||
periodType: true,
|
||||
smartContractAddress: true,
|
||||
periodDays: true,
|
||||
periodStartDate: true,
|
||||
periodEndDate: true,
|
||||
|
|
|
@ -108,7 +108,6 @@ const EventTypeList = ({ readOnly, types, profile }: EventTypeListProps): JSX.El
|
|||
ids: newList.map((type) => type.id),
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-16 -mx-4 overflow-hidden bg-white border border-gray-200 rounded-sm sm:mx-0">
|
||||
<ul className="divide-y divide-neutral-200" data-testid="event-types">
|
||||
|
|
|
@ -543,8 +543,35 @@ function IntegrationsContainer() {
|
|||
);
|
||||
}
|
||||
|
||||
function Web3Container() {
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<>
|
||||
<ShellSubHeading title="Web3" subtitle={t("meet_people_with_the_same_tokens")} />
|
||||
<div className="lg:pb-8 lg:col-span-9">
|
||||
<List>
|
||||
<ListItem className={classNames("flex-col")}>
|
||||
<div className={classNames("flex flex-1 space-x-2 w-full p-3 items-center")}>
|
||||
<Image width={40} height={40} src="/integrations/metamask.svg" alt="Embed" />
|
||||
<div className="flex-grow pl-2 truncate">
|
||||
<ListItemTitle component="h3">MetaMask</ListItemTitle>
|
||||
<ListItemText component="p">{t("only_book_people_and_allow")}</ListItemText>
|
||||
</div>
|
||||
<Button color="secondary" onClick={() => alert("activate web3 app")} data-testid="new_webhook">
|
||||
{t("connect")}
|
||||
</Button>
|
||||
</div>
|
||||
</ListItem>
|
||||
</List>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function IntegrationsPage() {
|
||||
const { t } = useLocale();
|
||||
const { data } = trpc.useQuery(["viewer.userSettings"]);
|
||||
|
||||
return (
|
||||
<Shell heading={t("integrations")} subtitle={t("connect_your_favourite_apps")}>
|
||||
|
@ -553,6 +580,7 @@ export default function IntegrationsPage() {
|
|||
<CalendarListContainer />
|
||||
<WebhookListContainer />
|
||||
<IframeEmbedContainer />
|
||||
{data?.web3App && <Web3Container />}
|
||||
</ClientSuspense>
|
||||
</Shell>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
-- RenameIndex
|
||||
ALTER INDEX "Booking_uid_key" RENAME TO "Booking.uid_unique";
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "DestinationCalendar_bookingId_key" RENAME TO "DestinationCalendar.bookingId_unique";
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "DestinationCalendar_eventTypeId_key" RENAME TO "DestinationCalendar.eventTypeId_unique";
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "DestinationCalendar_userId_key" RENAME TO "DestinationCalendar.userId_unique";
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "EventType_userId_slug_key" RENAME TO "EventType.userId_slug_unique";
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "Payment_externalId_key" RENAME TO "Payment.externalId_unique";
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "Payment_uid_key" RENAME TO "Payment.uid_unique";
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "Team_slug_key" RENAME TO "Team.slug_unique";
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "VerificationRequest_identifier_token_key" RENAME TO "VerificationRequest.identifier_token_unique";
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "VerificationRequest_token_key" RENAME TO "VerificationRequest.token_unique";
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "Webhook_id_key" RENAME TO "Webhook.id_unique";
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "users_email_key" RENAME TO "users.email_unique";
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "users_username_key" RENAME TO "users.username_unique";
|
|
@ -0,0 +1,2 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "EventType" ADD COLUMN "smartContractAddress" TEXT;
|
|
@ -0,0 +1,38 @@
|
|||
-- RenameIndex
|
||||
ALTER INDEX "Booking.uid_unique" RENAME TO "Booking_uid_key";
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "DestinationCalendar.bookingId_unique" RENAME TO "DestinationCalendar_bookingId_key";
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "DestinationCalendar.eventTypeId_unique" RENAME TO "DestinationCalendar_eventTypeId_key";
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "DestinationCalendar.userId_unique" RENAME TO "DestinationCalendar_userId_key";
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "EventType.userId_slug_unique" RENAME TO "EventType_userId_slug_key";
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "Payment.externalId_unique" RENAME TO "Payment_externalId_key";
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "Payment.uid_unique" RENAME TO "Payment_uid_key";
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "Team.slug_unique" RENAME TO "Team_slug_key";
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "VerificationRequest.identifier_token_unique" RENAME TO "VerificationRequest_identifier_token_key";
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "VerificationRequest.token_unique" RENAME TO "VerificationRequest_token_key";
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "Webhook.id_unique" RENAME TO "Webhook_id_key";
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "users.email_unique" RENAME TO "users_email_key";
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "users.username_unique" RENAME TO "users_username_key";
|
|
@ -0,0 +1,14 @@
|
|||
-- CreateTable
|
||||
CREATE TABLE "UserSettings" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"userId" INTEGER NOT NULL,
|
||||
"key" TEXT NOT NULL,
|
||||
"value" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "UserSettings_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "DailyEventReference_bookingId_unique" RENAME TO "DailyEventReference_bookingId_key";
|
|
@ -53,6 +53,7 @@ model EventType {
|
|||
price Int @default(0)
|
||||
currency String @default("usd")
|
||||
slotInterval Int?
|
||||
smartContractAddress String?
|
||||
|
||||
@@unique([userId, slug])
|
||||
}
|
||||
|
@ -194,7 +195,7 @@ model DailyEventReference {
|
|||
dailyurl String @default("dailycallurl")
|
||||
dailytoken String @default("dailytoken")
|
||||
booking Booking? @relation(fields: [bookingId], references: [id])
|
||||
bookingId Int?
|
||||
bookingId Int? @unique
|
||||
}
|
||||
|
||||
model Booking {
|
||||
|
@ -326,3 +327,12 @@ model Webhook {
|
|||
eventTriggers WebhookTriggerEvents[]
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
}
|
||||
|
||||
model UserSettings {
|
||||
id Int @id @default(autoincrement())
|
||||
userId Int
|
||||
key String
|
||||
value String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns:ev="http://www.w3.org/2001/xml-events"
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 318.6 318.6"
|
||||
style="enable-background:new 0 0 318.6 318.6;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#E2761B;stroke:#E2761B;stroke-linecap:round;stroke-linejoin:round;}
|
||||
.st1{fill:#E4761B;stroke:#E4761B;stroke-linecap:round;stroke-linejoin:round;}
|
||||
.st2{fill:#D7C1B3;stroke:#D7C1B3;stroke-linecap:round;stroke-linejoin:round;}
|
||||
.st3{fill:#233447;stroke:#233447;stroke-linecap:round;stroke-linejoin:round;}
|
||||
.st4{fill:#CD6116;stroke:#CD6116;stroke-linecap:round;stroke-linejoin:round;}
|
||||
.st5{fill:#E4751F;stroke:#E4751F;stroke-linecap:round;stroke-linejoin:round;}
|
||||
.st6{fill:#F6851B;stroke:#F6851B;stroke-linecap:round;stroke-linejoin:round;}
|
||||
.st7{fill:#C0AD9E;stroke:#C0AD9E;stroke-linecap:round;stroke-linejoin:round;}
|
||||
.st8{fill:#161616;stroke:#161616;stroke-linecap:round;stroke-linejoin:round;}
|
||||
.st9{fill:#763D16;stroke:#763D16;stroke-linecap:round;stroke-linejoin:round;}
|
||||
</style>
|
||||
<polygon class="st0" points="274.1,35.5 174.6,109.4 193,65.8 "/>
|
||||
<g>
|
||||
<polygon class="st1" points="44.4,35.5 143.1,110.1 125.6,65.8 "/>
|
||||
<polygon class="st1" points="238.3,206.8 211.8,247.4 268.5,263 284.8,207.7 "/>
|
||||
<polygon class="st1" points="33.9,207.7 50.1,263 106.8,247.4 80.3,206.8 "/>
|
||||
<polygon class="st1" points="103.6,138.2 87.8,162.1 144.1,164.6 142.1,104.1 "/>
|
||||
<polygon class="st1" points="214.9,138.2 175.9,103.4 174.6,164.6 230.8,162.1 "/>
|
||||
<polygon class="st1" points="106.8,247.4 140.6,230.9 111.4,208.1 "/>
|
||||
<polygon class="st1" points="177.9,230.9 211.8,247.4 207.1,208.1 "/>
|
||||
</g>
|
||||
<g>
|
||||
<polygon class="st2" points="211.8,247.4 177.9,230.9 180.6,253 180.3,262.3 "/>
|
||||
<polygon class="st2" points="106.8,247.4 138.3,262.3 138.1,253 140.6,230.9 "/>
|
||||
</g>
|
||||
<polygon class="st3" points="138.8,193.5 110.6,185.2 130.5,176.1 "/>
|
||||
<polygon class="st3" points="179.7,193.5 188,176.1 208,185.2 "/>
|
||||
<g>
|
||||
<polygon class="st4" points="106.8,247.4 111.6,206.8 80.3,207.7 "/>
|
||||
<polygon class="st4" points="207,206.8 211.8,247.4 238.3,207.7 "/>
|
||||
<polygon class="st4" points="230.8,162.1 174.6,164.6 179.8,193.5 188.1,176.1 208.1,185.2 "/>
|
||||
<polygon class="st4" points="110.6,185.2 130.6,176.1 138.8,193.5 144.1,164.6 87.8,162.1 "/>
|
||||
</g>
|
||||
<g>
|
||||
<polygon class="st5" points="87.8,162.1 111.4,208.1 110.6,185.2 "/>
|
||||
<polygon class="st5" points="208.1,185.2 207.1,208.1 230.8,162.1 "/>
|
||||
<polygon class="st5" points="144.1,164.6 138.8,193.5 145.4,227.6 146.9,182.7 "/>
|
||||
<polygon class="st5" points="174.6,164.6 171.9,182.6 173.1,227.6 179.8,193.5 "/>
|
||||
</g>
|
||||
<polygon class="st6" points="179.8,193.5 173.1,227.6 177.9,230.9 207.1,208.1 208.1,185.2 "/>
|
||||
<polygon class="st6" points="110.6,185.2 111.4,208.1 140.6,230.9 145.4,227.6 138.8,193.5 "/>
|
||||
<polygon class="st7" points="180.3,262.3 180.6,253 178.1,250.8 140.4,250.8 138.1,253 138.3,262.3 106.8,247.4 117.8,256.4
|
||||
140.1,271.9 178.4,271.9 200.8,256.4 211.8,247.4 "/>
|
||||
<polygon class="st8" points="177.9,230.9 173.1,227.6 145.4,227.6 140.6,230.9 138.1,253 140.4,250.8 178.1,250.8 180.6,253 "/>
|
||||
<g>
|
||||
<polygon class="st9" points="278.3,114.2 286.8,73.4 274.1,35.5 177.9,106.9 214.9,138.2 267.2,153.5 278.8,140 273.8,136.4
|
||||
281.8,129.1 275.6,124.3 283.6,118.2 "/>
|
||||
<polygon class="st9" points="31.8,73.4 40.3,114.2 34.9,118.2 42.9,124.3 36.8,129.1 44.8,136.4 39.8,140 51.3,153.5 103.6,138.2
|
||||
140.6,106.9 44.4,35.5 "/>
|
||||
</g>
|
||||
<polygon class="st6" points="267.2,153.5 214.9,138.2 230.8,162.1 207.1,208.1 238.3,207.7 284.8,207.7 "/>
|
||||
<polygon class="st6" points="103.6,138.2 51.3,153.5 33.9,207.7 80.3,207.7 111.4,208.1 87.8,162.1 "/>
|
||||
<polygon class="st6" points="174.6,164.6 177.9,106.9 193.1,65.8 125.6,65.8 140.6,106.9 144.1,164.6 145.3,182.8 145.4,227.6
|
||||
173.1,227.6 173.3,182.8 "/>
|
||||
</svg>
|
After Width: | Height: | Size: 3.9 KiB |
|
@ -0,0 +1,5 @@
|
|||
<svg width="390" height="390" viewBox="0 0 390 390" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="390" height="390" rx="60" fill="#4F46E5"/>
|
||||
<path d="M179.559 284.969C156.31 280.584 138.756 272.318 126.897 260.171C115.243 248.063 111.065 233.265 114.364 215.776L157.19 223.855C154.047 240.518 163.386 250.908 185.207 255.024C194.792 256.832 202.523 256.155 208.399 252.992C214.48 249.868 218.096 245.253 219.247 239.149C220.834 230.734 218.801 223.602 213.146 217.751C207.523 211.736 199.291 207.791 188.451 205.917L166.685 202.068L172.054 173.607L193.773 177.704C204.378 179.705 213.097 179.385 219.931 176.744C226.765 174.103 230.851 169.236 232.189 162.141C233.372 155.872 231.889 150.296 227.742 145.413C223.829 140.403 217.183 137.014 207.801 135.244C198.013 133.398 189.889 133.916 183.432 136.798C177.21 139.554 173.399 144.645 171.998 152.069L130.701 144.279C133.813 127.78 142.767 116.142 157.562 109.365C172.592 102.461 191.221 101.106 213.45 105.299C227.93 108.031 239.95 112.604 249.51 119.021C259.306 125.311 266.241 132.77 270.315 141.398C274.39 150.026 275.54 159.042 273.767 168.446C270.499 185.77 259.748 196.3 241.514 200.037C258.751 211.66 265.394 227.948 261.441 248.902C259.667 258.306 255.209 266.264 248.068 272.776C240.957 279.124 231.475 283.486 219.622 285.863C208.004 288.114 194.65 287.816 179.559 284.969Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M230.875 87.5969L201.204 82L193.942 120.498H201.338C209.298 120.498 216.895 122.048 223.846 124.862L230.875 87.5969ZM194.674 279.51H186.596C178.878 279.51 171.5 278.053 164.723 275.398L159.669 302.187L189.341 307.784L194.674 279.51Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
|
@ -570,5 +570,7 @@
|
|||
"error_required_field": "This field is required.",
|
||||
"status": "Status",
|
||||
"team_view_user_availability": "View user availability",
|
||||
"team_view_user_availability_disabled": "User needs to accept invite to view availability"
|
||||
"team_view_user_availability_disabled": "User needs to accept invite to view availability",
|
||||
"meet_people_with_the_same_tokens": "Meet people with the same tokens",
|
||||
"only_book_people_and_allow": "Only book and allow bookings from people who share the same tokens, DAOs, or NFTs."
|
||||
}
|
||||
|
|
|
@ -499,6 +499,22 @@ const loggedInViewerRouter = createProtectedRouter()
|
|||
};
|
||||
},
|
||||
})
|
||||
.query("userSettings", {
|
||||
async resolve({ ctx }) {
|
||||
const { prisma, user } = ctx;
|
||||
|
||||
const web3AppConfiguration = await prisma.userSettings.findFirst({
|
||||
where: {
|
||||
userId: user.id,
|
||||
key: "web3App",
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
web3App: (web3AppConfiguration?.value || "false") === "true",
|
||||
};
|
||||
},
|
||||
})
|
||||
.mutation("updateProfile", {
|
||||
input: z.object({
|
||||
username: z.string().optional(),
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,9 @@
|
|||
[
|
||||
{
|
||||
"inputs": [{ "internalType": "address", "name": "owner", "type": "address" }],
|
||||
"name": "balanceOf",
|
||||
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
}
|
||||
]
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue