Compare commits

...

16 Commits

Author SHA1 Message Date
Edward Fernandez 20beb961c6 Add web3app validation using user settings 2022-01-10 15:19:53 -05:00
Edward Fernandez 036224f7bd Add user_settings table 2022-01-10 09:14:38 -05:00
kodiakhq[bot] cf8f43ee6d
Merge branch 'main' into web3 2022-01-08 16:54:46 +00:00
Peer Richelsen 1f65bc77f5 merge 2022-01-07 23:05:20 +00:00
Yuval Drori 38886e81ca convert axios to fetch, change scAddress to smartContractAddress, load bloxy from next_public_env 2022-01-07 22:18:49 +01:00
kodiakhq[bot] cb2842c695
Merge branch 'main' into web3 2022-01-03 22:51:34 +00:00
kodiakhq[bot] 7f2323a0d6
Merge branch 'main' into web3 2022-01-03 12:02:09 +00:00
Yuval Drori 12c728b4e4
Update components/CryptoSection.tsx
Change comment from // to /** as @zomars suggested

Co-authored-by: Omar López <zomars@me.com>
2021-12-30 22:00:41 +00:00
kodiakhq[bot] a5e37fe8b6
Merge branch 'main' into web3 2021-12-30 18:17:32 +00:00
Yuval Drori 9ed4295b28 Merge branch 'web3' of https://github.com/calendso/calendso into web3
extract api response to const
2021-12-30 19:09:22 +01:00
Yuval Drori cf75791823 extract to api 2021-12-30 19:09:07 +01:00
Omar López 2ada162c90
Update eventType.ts 2021-12-30 09:40:09 -07:00
Peer Richelsen f670cc23f2 added web3 wrapper, needs connection to user_settings db 2021-12-30 14:36:27 +01:00
Peer Richelsen 59720b8ca7 added web3 section to integrations 2021-12-30 14:28:08 +01:00
Peer Richelsen 3847196aa6
Merge branch 'main' into web3 2021-12-30 13:33:56 +01:00
Yuval Drori 45eb4422e3
Crypto events (#1390)
* update schemas, functions & ui to allow creating and updating events with a smart contract property

* remove adding sc address in the dialog that first pops-up when creating a new event, since its an advanced option

* add sc to booking ui

* some more ts && error handling

* fetch erc20s and nfts list in event-type page

* some cleanup within time limit

* ts fix 1

* more ts fixes
2021-12-30 13:32:59 +01:00
28 changed files with 10488 additions and 1171 deletions

View File

@ -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=

View File

@ -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;

View File

@ -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">

View File

@ -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;

View File

@ -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": {

View File

@ -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,
});

View File

@ -44,6 +44,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
schedulingType: true,
minimumBookingNotice: true,
timeZone: true,
smartContractAddress: true,
slotInterval: true,
users: {
select: {

View File

@ -56,6 +56,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
periodDays: true,
periodStartDate: true,
periodEndDate: true,
smartContractAddress: true,
periodCountCalendarDays: true,
price: true,
currency: true,

View File

@ -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) {

View File

@ -224,6 +224,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
userId: true,
price: true,
currency: true,
smartContractAddress: true,
},
});

View File

@ -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 });
}
}

View File

@ -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 });

View File

@ -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,

View File

@ -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">

View File

@ -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>
);

View File

@ -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";

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "EventType" ADD COLUMN "smartContractAddress" TEXT;

View File

@ -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";

View File

@ -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";

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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."
}

View File

@ -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(),

1
tsconfig.tsbuildinfo Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,9 @@
[
{
"inputs": [{ "internalType": "address", "name": "owner", "type": "address" }],
"name": "balanceOf",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
}
]

7004
web3/dummyResps/bloxyApi.js Normal file

File diff suppressed because it is too large Load Diff

4108
yarn.lock

File diff suppressed because it is too large Load Diff