From 1d1087489017141d239af3e99900b7c6e9e9e634 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Edward=20Fern=C3=A1ndez?=
<40343753+edanfesi@users.noreply.github.com>
Date: Tue, 1 Feb 2022 16:48:40 -0500
Subject: [PATCH] Web3 App (#1603)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* 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
* added web3 section to integrations
* added web3 wrapper, needs connection to user_settings db
* extract to api
* Update eventType.ts
* Update components/CryptoSection.tsx
Change comment from // to /** as @zomars suggested
Co-authored-by: Omar López
* convert axios to fetch, change scAddress to smartContractAddress, load bloxy from next_public_env
* Fix branch conflict
* add enable/disable btn web3
* fixed away user causing duplicate entries
* Remove web3 validation
* renamed web3 button in integrations
* remove unused variable
* Add metadata column
* added loader and showToast to the web3 btn
* fix: remove smartContractAddress from info sended
* send to user events when the contract is missing
* use window.web3 instead of web3
* use NEXT_PUBLIC_WEB3_AUTH_MSG
* remove web3 auth from .env
* wip
* wip
* Add metamask not installed msg and success redirect
* add redirect when verified
* styled web3 button and added i18n to web3
* fixed redirect after verification
* wip
* wip
* moved crypto section to ee
Co-authored-by: Yuval Drori <53199044+yuvd@users.noreply.github.com>
Co-authored-by: Peer Richelsen
Co-authored-by: Yuval Drori
Co-authored-by: Omar López
Co-authored-by: Edward Fernandez
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Peer Richelsen
---
.env.example | 5 +
components/NavTabs.tsx | 4 +-
components/booking/pages/AvailabilityPage.tsx | 10 +
components/booking/pages/BookingPage.tsx | 59 +-
contexts/contractsContext.tsx | 42 +
ee/components/web3/CryptoSection.tsx | 149 +
lib/integrations/getIntegrations.ts | 13 +-
lib/types/event-type.ts | 2 +
package.json | 6 +-
pages/[user].tsx | 74 +-
pages/[user]/[type].tsx | 5 +
pages/[user]/book.tsx | 30 +-
pages/_app.tsx | 13 +-
pages/api/book/event.ts | 11 +-
pages/api/event-type/index.ts | 3 +-
pages/api/eventType.ts | 5 +-
pages/event-types/[type].tsx | 112 +-
pages/integrations/index.tsx | 85 +-
pages/team/[slug]/[type].tsx | 3 +
pages/team/[slug]/book.tsx | 3 +
.../20211222174947_placeholder/migration.sql | 38 +
.../migration.sql | 2 +
.../migration.sql | 3 +
.../migration.sql | 1 +
prisma/schema.prisma | 3 +-
prisma/zod/eventtype.ts | 1 +
public/integrations/metamask.svg | 61 +
public/integrations/web3.svg | 5 +
public/static/locales/de/common.json | 2 +
public/static/locales/en/common.json | 11 +-
public/static/locales/es/common.json | 2 +
public/static/locales/fr/common.json | 2 +
public/static/locales/it/common.json | 2 +
public/static/locales/ja/common.json | 2 +
public/static/locales/ko/common.json | 2 +
public/static/locales/nl/common.json | 2 +
public/static/locales/pl/common.json | 2 +
public/static/locales/pt-BR/common.json | 2 +
public/static/locales/pt/common.json | 2 +
public/static/locales/ro/common.json | 2 +
public/static/locales/ru/common.json | 2 +
public/static/locales/zh-CN/common.json | 2 +
server/routers/viewer.tsx | 57 +
tsconfig.tsbuildinfo | 1 +
web3/abis/abiWithGetBalance.json | 9 +
web3/dummyResps/bloxyApi.js | 7004 +++++++++++++++++
web3/utils/verifyAccount.ts | 12 +
yarn.lock | 4171 ++++++----
48 files changed, 10630 insertions(+), 1409 deletions(-)
create mode 100644 contexts/contractsContext.tsx
create mode 100644 ee/components/web3/CryptoSection.tsx
create mode 100644 prisma/migrations/20211222174947_placeholder/migration.sql
create mode 100644 prisma/migrations/20211222181246_add_sc_address/migration.sql
create mode 100644 prisma/migrations/20220113145333_rename_column_sc_address_to_smart_contract_address/migration.sql
create mode 100644 prisma/migrations/20220131170110_add_metadata_column_to_event_type/migration.sql
create mode 100644 public/integrations/metamask.svg
create mode 100644 public/integrations/web3.svg
create mode 100644 tsconfig.tsbuildinfo
create mode 100644 web3/abis/abiWithGetBalance.json
create mode 100644 web3/dummyResps/bloxyApi.js
create mode 100644 web3/utils/verifyAccount.ts
diff --git a/.env.example b/.env.example
index b28da85c..e9f26198 100644
--- a/.env.example
+++ b/.env.example
@@ -90,3 +90,8 @@ CALENDSO_ENCRYPTION_KEY=
# Intercom Config
NEXT_PUBLIC_INTERCOM_APP_ID=
+
+# Web3/Crypto stuff
+NEXT_PUBLIC_BLOXY_API_KEY=
+# Auth message can be whatever you want, doesn't really matter because it's encrypted with a private key anyway, and will be visible to the signee
+NEXT_PUBLIC_WEB3_AUTH_MSG=
diff --git a/components/NavTabs.tsx b/components/NavTabs.tsx
index d8e66a18..ce6a0560 100644
--- a/components/NavTabs.tsx
+++ b/components/NavTabs.tsx
@@ -17,11 +17,11 @@ const NavTabs: FC = ({ tabs, linkProps }) => {
const router = useRouter();
return (
<>
-
+ {eventTypeDetail.isWeb3Active && eventType.metadata.smartContractAddress && (
+
+ Requires ownership of a token belonging to the following address:{" "}
+ {eventType.metadata.smartContractAddress}
+
+ )}
{props.eventType.description}
diff --git a/contexts/contractsContext.tsx b/contexts/contractsContext.tsx
new file mode 100644
index 00000000..9d5beb63
--- /dev/null
+++ b/contexts/contractsContext.tsx
@@ -0,0 +1,42 @@
+import { createContext, ReactNode, useContext, useState } from "react";
+
+type contractsContextType = Record
;
+
+const contractsContextDefaultValue: contractsContextType = {};
+
+const ContractsContext = createContext(contractsContextDefaultValue);
+
+export function useContracts() {
+ return useContext(ContractsContext);
+}
+
+type Props = {
+ children: ReactNode;
+};
+
+interface addContractsPayload {
+ address: string;
+ signature: string;
+}
+
+export function ContractsProvider({ children }: Props) {
+ const [contracts, setContracts] = useState>({});
+
+ const addContract = (payload: addContractsPayload) => {
+ setContracts((prevContracts) => ({
+ ...prevContracts,
+ [payload.address]: payload.signature,
+ }));
+ };
+
+ const value = {
+ contracts,
+ addContract,
+ };
+
+ return (
+ <>
+ {children}
+ >
+ );
+}
diff --git a/ee/components/web3/CryptoSection.tsx b/ee/components/web3/CryptoSection.tsx
new file mode 100644
index 00000000..c6098171
--- /dev/null
+++ b/ee/components/web3/CryptoSection.tsx
@@ -0,0 +1,149 @@
+import Router from "next/router";
+import { useCallback, useMemo, useState } from "react";
+import React from "react";
+import Web3 from "web3";
+import { AbiItem } from "web3-utils";
+import verifyAccount, { AUTH_MESSAGE } from "web3/utils/verifyAccount";
+
+import { useLocale } from "@lib/hooks/useLocale";
+import showToast from "@lib/notification";
+
+import { Button } from "@components/ui/Button";
+
+import { useContracts } from "../../../contexts/contractsContext";
+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;
+ pathname: 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>>;
+}
+
+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(false);
+ const { addContract } = useContracts();
+ const { t } = useLocale();
+
+ 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 {
+ if (!window.web3) {
+ throw new Error("Metamask is not installed");
+ }
+
+ 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;
+
+ if (!hasToken) {
+ throw new Error("Specified wallet does not own any tokens belonging to this smart contract");
+ } else {
+ const account = (await web3.eth.getAccounts())[0];
+
+ const signature = await window.web3.eth.personal.sign(AUTH_MESSAGE, account);
+ addContract({ address: props.smartContractAddress, signature });
+
+ await verifyAccount(signature, account);
+
+ props.setEvtsToVerify((prevState: EvtsToVerify) => {
+ const changedEvt = { [props.id]: hasToken };
+ return { ...prevState, ...changedEvt };
+ });
+ }
+ } catch (err) {
+ err instanceof Error ? showToast(err.message, "error") : showToast("An error has occurred", "error");
+ }
+ }, [props, addContract]);
+
+ // @TODO: Show error on either of buttons if fails. Yup schema already contains the error message.
+ const successButton = useMemo(() => {
+ console.log(props);
+ if (props.verified) {
+ Router.push(props.pathname);
+ }
+
+ return ;
+ }, [props.verified]);
+
+ const verifyButton = useMemo(() => {
+ return (
+
+ );
+ }, [verifyWallet, t]);
+
+ const connectButton = useMemo(() => {
+ return (
+
+ );
+ }, [connectMetamask, t]);
+
+ const oneStepButton = useMemo(() => {
+ return (
+
+ );
+ }, [connectMetamask, verifyWallet, t]);
+
+ 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 (
+
+ {determineButton()}
+
+ );
+};
+
+export default CryptoSection;
diff --git a/lib/integrations/getIntegrations.ts b/lib/integrations/getIntegrations.ts
index 9ebedd67..266f56c6 100644
--- a/lib/integrations/getIntegrations.ts
+++ b/lib/integrations/getIntegrations.ts
@@ -22,11 +22,12 @@ export type Integration = {
| "daily_video"
| "caldav_calendar"
| "apple_calendar"
- | "stripe_payment";
+ | "stripe_payment"
+ | "metamask_web3";
title: string;
imageSrc: string;
description: string;
- variant: "calendar" | "conferencing" | "payment";
+ variant: "calendar" | "conferencing" | "payment" | "web3";
};
export const ALL_INTEGRATIONS = [
@@ -90,6 +91,14 @@ export const ALL_INTEGRATIONS = [
description: "Collect payments",
variant: "payment",
},
+ {
+ installed: true,
+ type: "metamask_web3",
+ title: "Metamask",
+ imageSrc: "integrations/apple-calendar.svg",
+ description: "For personal and business calendars",
+ variant: "web3",
+ },
] as Integration[];
function getIntegrations(userCredentials: CredentialData[]) {
diff --git a/lib/types/event-type.ts b/lib/types/event-type.ts
index 38b2148c..14d3b46b 100644
--- a/lib/types/event-type.ts
+++ b/lib/types/event-type.ts
@@ -1,8 +1,10 @@
import { EventType, SchedulingType } from "@prisma/client";
+import { JSONObject } from "superjson/dist/types";
import { WorkingHours } from "./schedule";
export type AdvancedOptions = {
+ metadata?: JSONObject;
eventName?: string;
periodType?: string;
periodDays?: number;
diff --git a/package.json b/package.json
index 7a3009d2..8d1a2386 100644
--- a/package.json
+++ b/package.json
@@ -27,6 +27,7 @@
"postinstall": "yarn generate-schemas",
"pre-commit": "lint-staged",
"lint": "eslint . --ext .ts,.js,.tsx,.jsx",
+ "lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
"prepare": "husky install",
"check-changed-files": "ts-node scripts/ts-check-changed-files.ts"
},
@@ -39,8 +40,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",
@@ -103,6 +106,7 @@
"tsdav": "2.0.0-rc.3",
"tslog": "^3.2.1",
"uuid": "^8.3.2",
+ "web3": "^1.6.1",
"zod": "^3.8.2"
},
"devDependencies": {
diff --git a/pages/[user].tsx b/pages/[user].tsx
index 0e411b16..5f9b9805 100644
--- a/pages/[user].tsx
+++ b/pages/[user].tsx
@@ -1,12 +1,14 @@
import { ArrowRightIcon } from "@heroicons/react/outline";
-import { MoonIcon } from "@heroicons/react/solid";
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 { JSONObject } from "superjson/dist/types";
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";
@@ -16,6 +18,12 @@ import Avatar from "@components/ui/Avatar";
import { ssrInit } from "@server/lib/ssr";
+import CryptoSection from "../ee/components/web3/CryptoSection";
+
+interface EvtsToVerify {
+ [evtId: string]: boolean;
+}
+
export default function User(props: inferSSRProps) {
const { isReady } = useTheme(props.user.theme);
const { user, eventTypes } = props;
@@ -26,6 +34,8 @@ export default function User(props: inferSSRProps) {
const nameOrUsername = user.name || user.username || "";
+ const [evtsToVerify, setEvtsToVerify] = useState({});
+
return (
<>
) {
{user.bio}
- {user.away && (
-
-
-
{t("user_away")}
-
{t("user_away_description")}
-
- )}
{!user.away &&
eventTypes.map((type) => (
))}
@@ -87,6 +113,7 @@ export default function User(props: inferSSRProps) {
)}
+
)}
>
@@ -121,6 +148,19 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
};
}
+ const credentials = await prisma.credential.findMany({
+ where: {
+ userId: user.id,
+ },
+ select: {
+ id: true,
+ type: true,
+ key: true,
+ },
+ });
+
+ const web3Credentials = credentials.find((credential) => credential.type.includes("_web3"));
+
const eventTypesWithHidden = await prisma.eventType.findMany({
where: {
AND: [
@@ -161,11 +201,21 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
schedulingType: true,
price: true,
currency: true,
+ metadata: true,
},
take: user.plan === "FREE" ? 1 : undefined,
});
- const eventTypes = eventTypesWithHidden.filter((evt) => !evt.hidden);
+ const eventTypesRaw = eventTypesWithHidden.filter((evt) => !evt.hidden);
+
+ const eventTypes = eventTypesRaw.map((eventType) => ({
+ ...eventType,
+ metadata: (eventType.metadata || {}) as JSONObject,
+ isWeb3Active:
+ web3Credentials && web3Credentials.key
+ ? (((web3Credentials.key as JSONObject).isWeb3Active || false) as boolean)
+ : false,
+ }));
return {
props: {
diff --git a/pages/[user]/[type].tsx b/pages/[user]/[type].tsx
index 6702d1e2..59dd149a 100644
--- a/pages/[user]/[type].tsx
+++ b/pages/[user]/[type].tsx
@@ -1,5 +1,6 @@
import { Prisma } from "@prisma/client";
import { GetServerSidePropsContext } from "next";
+import { JSONObject } from "superjson/dist/types";
import { asStringOrNull } from "@lib/asStringOrNull";
import { getWorkingHours } from "@lib/availability";
@@ -44,6 +45,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
schedulingType: true,
minimumBookingNotice: true,
timeZone: true,
+ metadata: true,
slotInterval: true,
users: {
select: {
@@ -118,6 +120,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
notFound: true,
};
}
+
eventTypeBackwardsCompat.users.push({
avatar: user.avatar,
name: user.name,
@@ -126,6 +129,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
plan: user.plan,
timeZone: user.timeZone,
});
+
user.eventTypes.push(eventTypeBackwardsCompat);
}
@@ -163,6 +167,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
}*/
const eventTypeObject = Object.assign({}, eventType, {
+ metadata: (eventType.metadata || {}) as JSONObject,
periodStartDate: eventType.periodStartDate?.toString() ?? null,
periodEndDate: eventType.periodEndDate?.toString() ?? null,
});
diff --git a/pages/[user]/book.tsx b/pages/[user]/book.tsx
index 54482310..9d1d61dc 100644
--- a/pages/[user]/book.tsx
+++ b/pages/[user]/book.tsx
@@ -2,6 +2,7 @@ import dayjs from "dayjs";
import timezone from "dayjs/plugin/timezone";
import utc from "dayjs/plugin/utc";
import { GetServerSidePropsContext } from "next";
+import { JSONObject } from "superjson/dist/types";
import { asStringOrThrow } from "@lib/asStringOrNull";
import prisma from "@lib/prisma";
@@ -22,12 +23,12 @@ export default function Book(props: BookPageProps) {
export async function getServerSideProps(context: GetServerSidePropsContext) {
const ssr = await ssrInit(context);
-
const user = await prisma.user.findUnique({
where: {
username: asStringOrThrow(context.query.user),
},
select: {
+ id: true,
username: true,
name: true,
email: true,
@@ -40,7 +41,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
if (!user) return { notFound: true };
- const eventType = await prisma.eventType.findUnique({
+ const eventTypeRaw = await prisma.eventType.findUnique({
where: {
id: parseInt(asStringOrThrow(context.query.type)),
},
@@ -56,6 +57,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
periodDays: true,
periodStartDate: true,
periodEndDate: true,
+ metadata: true,
periodCountCalendarDays: true,
price: true,
currency: true,
@@ -73,7 +75,29 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
},
});
- if (!eventType) return { notFound: true };
+ if (!eventTypeRaw) return { notFound: true };
+
+ const credentials = await prisma.credential.findMany({
+ where: {
+ userId: user.id,
+ },
+ select: {
+ id: true,
+ type: true,
+ key: true,
+ },
+ });
+
+ const web3Credentials = credentials.find((credential) => credential.type.includes("_web3"));
+
+ const eventType = {
+ ...eventTypeRaw,
+ metadata: (eventTypeRaw.metadata || {}) as JSONObject,
+ isWeb3Active:
+ web3Credentials && web3Credentials.key
+ ? (((web3Credentials.key as JSONObject).isWeb3Active || false) as boolean)
+ : false,
+ };
const eventTypeObject = [eventType].map((e) => {
return {
diff --git a/pages/_app.tsx b/pages/_app.tsx
index c65bf95e..cb9e121a 100644
--- a/pages/_app.tsx
+++ b/pages/_app.tsx
@@ -13,17 +13,20 @@ import { withTRPC } from "@trpc/next";
import type { TRPCClientErrorLike } from "@trpc/react";
import { Maybe } from "@trpc/server";
+import { ContractsProvider } from "../contexts/contractsContext";
import "../styles/fonts.css";
import "../styles/globals.css";
function MyApp(props: AppProps) {
const { Component, pageProps, err } = props;
return (
-
-
-
-
-
+
+
+
+
+
+
+
);
}
diff --git a/pages/api/book/event.ts b/pages/api/book/event.ts
index d5766ef7..468fb39c 100644
--- a/pages/api/book/event.ts
+++ b/pages/api/book/event.ts
@@ -8,6 +8,7 @@ import utc from "dayjs/plugin/utc";
import type { NextApiRequest, NextApiResponse } from "next";
import short from "short-uuid";
import { v5 as uuidv5 } from "uuid";
+import verifyAccount from "web3/utils/verifyAccount";
import { handlePayment } from "@ee/lib/stripe/server";
@@ -102,6 +103,7 @@ function isAvailable(busyTimes: BufferedBusyTimes, time: string, length: number)
function isOutOfBounds(
time: dayjs.ConfigType,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
{ periodType, periodDays, periodCountCalendarDays, periodStartDate, periodEndDate, timeZone }: any // FIXME types
): boolean {
const date = dayjs(time);
@@ -226,6 +228,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
userId: true,
price: true,
currency: true,
+ metadata: true,
destinationCalendar: true,
},
});
@@ -349,7 +352,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
// Initialize EventManager with credentials
const rescheduleUid = reqBody.rescheduleUid;
- function createBooking() {
+ async function createBooking() {
+ // @TODO: check as metadata
+ if (req.body.web3Details) {
+ const { web3Details } = req.body;
+ await verifyAccount(web3Details.userSignature, web3Details.userWallet);
+ }
+
return prisma.booking.create({
include: {
user: {
diff --git a/pages/api/event-type/index.ts b/pages/api/event-type/index.ts
index fdaf69d8..a643af4f 100644
--- a/pages/api/event-type/index.ts
+++ b/pages/api/event-type/index.ts
@@ -35,11 +35,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
schedulingType: true,
slug: true,
hidden: true,
+ metadata: true,
},
},
},
});
- return res.status(200).json({ message: "Events.", data: user.eventTypes });
+ return res.status(200).json({ message: "Events.", data: user?.eventTypes });
}
}
diff --git a/pages/api/eventType.ts b/pages/api/eventType.ts
index f4be6d1e..4ab41d9b 100644
--- a/pages/api/eventType.ts
+++ b/pages/api/eventType.ts
@@ -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,
+ metadata: true,
users: {
select: {
id: true,
@@ -109,7 +110,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}, {} as Record);
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 });
diff --git a/pages/event-types/[type].tsx b/pages/event-types/[type].tsx
index 056d69ff..18b16d91 100644
--- a/pages/event-types/[type].tsx
+++ b/pages/event-types/[type].tsx
@@ -24,6 +24,7 @@ import React, { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { FormattedNumber, IntlProvider } from "react-intl";
import Select from "react-select";
+import { JSONObject } from "superjson/dist/types";
import { StripeData } from "@ee/lib/stripe/server";
@@ -55,9 +56,21 @@ 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;
+}
type AvailabilityInput = Pick;
type OptionTypeBase = {
@@ -145,6 +158,7 @@ const EventTypePage = (props: inferSSRProps) => {
const [customInputs, setCustomInputs] = useState(
eventType.customInputs.sort((a, b) => a.id - b.id) || []
);
+ const [tokensList, setTokensList] = useState>([]);
const periodType =
PERIOD_TYPES.find((s) => s.type === eventType.periodType) ||
@@ -153,6 +167,41 @@ const EventTypePage = (props: inferSSRProps) => {
const [requirePayment, setRequirePayment] = useState(eventType.price > 0);
const [advancedSettingsVisible, setAdvancedSettingsVisible] = useState(false);
+ useEffect(() => {
+ 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 =
+ // 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 = exodiaList.map((nft: NFT) => {
+ const { name, contracts } = nft;
+ if (nft.contracts[0]) {
+ const { address, symbol } = contracts[0];
+ return { name, address, symbol };
+ }
+ });
+
+ const unifiedList: Array = [...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 || "");
}, []);
@@ -261,6 +310,8 @@ const EventTypePage = (props: inferSSRProps) => {
const formMethods = useForm<{
title: string;
+ eventTitle: string;
+ smartContractAddress: string;
eventName: string;
slug: string;
length: number;
@@ -512,9 +563,13 @@ const EventTypePage = (props: inferSSRProps) => {