Web3 App (#1603)
* 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 <zomars@me.com> * 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 <peeroke@richelsen.net> Co-authored-by: Yuval Drori <yuvald29@protonmail.com> Co-authored-by: Omar López <zomars@me.com> Co-authored-by: Edward Fernandez <edward.fernandez@rappi.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: Peer Richelsen <peeroke@gmail.com>
This commit is contained in:
parent
a81bb67cb1
commit
1d10874890
48 changed files with 10630 additions and 1409 deletions
|
@ -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=
|
||||
|
|
|
@ -17,11 +17,11 @@ const NavTabs: FC<Props> = ({ tabs, linkProps }) => {
|
|||
const router = useRouter();
|
||||
return (
|
||||
<>
|
||||
<nav className="-mb-px flex space-x-2 sm:space-x-5" aria-label="Tabs">
|
||||
<nav className="flex -mb-px space-x-2 sm:space-x-5" aria-label="Tabs">
|
||||
{tabs.map((tab) => {
|
||||
const isCurrent = router.asPath === tab.href;
|
||||
return (
|
||||
<Link {...linkProps} key={tab.name} href={tab.href}>
|
||||
<Link key={tab.name} href={tab.href} {...linkProps}>
|
||||
<a
|
||||
className={classNames(
|
||||
isCurrent
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// Get router variables
|
||||
import { ChevronDownIcon, ChevronUpIcon, ClockIcon, CreditCardIcon, GlobeIcon } from "@heroicons/react/solid";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { useContracts } from "contexts/contractsContext";
|
||||
import dayjs, { Dayjs } from "dayjs";
|
||||
import customParseFormat from "dayjs/plugin/customParseFormat";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
|
@ -36,6 +37,15 @@ const AvailabilityPage = ({ profile, eventType, workingHours }: Props) => {
|
|||
const { rescheduleUid } = router.query;
|
||||
const { isReady } = useTheme(profile.theme);
|
||||
const { t } = useLocale();
|
||||
const { contracts } = useContracts();
|
||||
|
||||
useEffect(() => {
|
||||
if (eventType.metadata.smartContractAddress) {
|
||||
const eventOwner = eventType.users[0];
|
||||
if (!contracts[(eventType.metadata.smartContractAddress || null) as number])
|
||||
router.replace(`/${eventOwner.username}`);
|
||||
}
|
||||
}, [contracts, eventType.metadata.smartContractAddress, router]);
|
||||
|
||||
const selectedDate = useMemo(() => {
|
||||
const dateString = asStringOrNull(router.query.date);
|
||||
|
|
|
@ -6,11 +6,12 @@ import {
|
|||
LocationMarkerIcon,
|
||||
} from "@heroicons/react/solid";
|
||||
import { EventTypeCustomInputType } from "@prisma/client";
|
||||
import { useContracts } from "contexts/contractsContext";
|
||||
import dayjs from "dayjs";
|
||||
import dynamic from "next/dynamic";
|
||||
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";
|
||||
|
@ -42,9 +43,33 @@ const PhoneInput = dynamic(() => import("@components/ui/form/PhoneInput"));
|
|||
|
||||
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();
|
||||
const { contracts } = useContracts();
|
||||
|
||||
const { eventType } = props;
|
||||
useEffect(() => {
|
||||
if (eventType.metadata.smartContractAddress) {
|
||||
const eventOwner = eventType.users[0];
|
||||
|
||||
if (!contracts[(eventType.metadata.smartContractAddress || null) as number])
|
||||
router.replace(`/${eventOwner.username}`);
|
||||
}
|
||||
}, [contracts, eventType.metadata.smartContractAddress, router]);
|
||||
|
||||
/*
|
||||
* This was too optimistic
|
||||
* I started, then I remembered what a beast book/event.ts is
|
||||
|
@ -55,6 +80,7 @@ const BookingPage = (props: BookingPageProps) => {
|
|||
// go to success page.
|
||||
},
|
||||
});*/
|
||||
|
||||
const mutation = useMutation(createBooking, {
|
||||
onSuccess: async ({ attendees, paymentUid, ...responseData }) => {
|
||||
if (paymentUid) {
|
||||
|
@ -101,6 +127,8 @@ const BookingPage = (props: BookingPageProps) => {
|
|||
|
||||
const [guestToggle, setGuestToggle] = useState(props.booking && props.booking.attendees.length > 1);
|
||||
|
||||
const eventTypeDetail = { isWeb3Active: false, ...props.eventType };
|
||||
|
||||
type Location = { type: LocationType; address?: string };
|
||||
// it would be nice if Prisma at some point in the future allowed for Json<Location>; as of now this is not the case.
|
||||
const locations: Location[] = useMemo(
|
||||
|
@ -127,18 +155,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 defaultValues = () => {
|
||||
if (!rescheduleUid) {
|
||||
return {
|
||||
|
@ -216,6 +232,8 @@ const BookingPage = (props: BookingPageProps) => {
|
|||
|
||||
// "metadata" is a reserved key to allow for connecting external users without relying on the email address.
|
||||
// <...url>&metadata[user_id]=123 will be send as a custom input field as the hidden type.
|
||||
|
||||
// @TODO: move to metadata
|
||||
const metadata = Object.keys(router.query)
|
||||
.filter((key) => key.startsWith("metadata"))
|
||||
.reduce(
|
||||
|
@ -226,8 +244,17 @@ const BookingPage = (props: BookingPageProps) => {
|
|||
{}
|
||||
);
|
||||
|
||||
let web3Details;
|
||||
if (eventTypeDetail.metadata.smartContractAddress) {
|
||||
web3Details = {
|
||||
userWallet: web3.currentProvider.selectedAddress,
|
||||
userSignature: contracts[(eventTypeDetail.metadata.smartContractAddress || null) as number],
|
||||
};
|
||||
}
|
||||
|
||||
mutation.mutate({
|
||||
...booking,
|
||||
web3Details,
|
||||
start: dayjs(date).format(),
|
||||
end: dayjs(date).add(props.eventType.length, "minute").format(),
|
||||
eventTypeId: props.eventType.id,
|
||||
|
@ -312,6 +339,12 @@ const BookingPage = (props: BookingPageProps) => {
|
|||
<CalendarIcon className="inline-block w-4 h-4 mr-1 -mt-1" />
|
||||
{parseDate(date)}
|
||||
</p>
|
||||
{eventTypeDetail.isWeb3Active && eventType.metadata.smartContractAddress && (
|
||||
<p className="px-2 py-1 mb-1 -ml-2 text-gray-500">
|
||||
Requires ownership of a token belonging to the following address:{" "}
|
||||
{eventType.metadata.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">
|
||||
|
|
42
contexts/contractsContext.tsx
Normal file
42
contexts/contractsContext.tsx
Normal file
|
@ -0,0 +1,42 @@
|
|||
import { createContext, ReactNode, useContext, useState } from "react";
|
||||
|
||||
type contractsContextType = Record<string, string>;
|
||||
|
||||
const contractsContextDefaultValue: contractsContextType = {};
|
||||
|
||||
const ContractsContext = createContext<contractsContextType>(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<Record<string, string>>({});
|
||||
|
||||
const addContract = (payload: addContractsPayload) => {
|
||||
setContracts((prevContracts) => ({
|
||||
...prevContracts,
|
||||
[payload.address]: payload.signature,
|
||||
}));
|
||||
};
|
||||
|
||||
const value = {
|
||||
contracts,
|
||||
addContract,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContractsContext.Provider value={value}>{children}</ContractsContext.Provider>
|
||||
</>
|
||||
);
|
||||
}
|
149
ee/components/web3/CryptoSection.tsx
Normal file
149
ee/components/web3/CryptoSection.tsx
Normal file
|
@ -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<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 { 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 <div />;
|
||||
}, [props.verified]);
|
||||
|
||||
const verifyButton = useMemo(() => {
|
||||
return (
|
||||
<Button color="secondary" onClick={verifyWallet} type="button" id="hasToken" name="hasToken">
|
||||
<img className="h-5 mr-1" src="/integrations/metamask.svg" />
|
||||
{t("verify_wallet")}
|
||||
</Button>
|
||||
);
|
||||
}, [verifyWallet, t]);
|
||||
|
||||
const connectButton = useMemo(() => {
|
||||
return (
|
||||
<Button color="secondary" onClick={connectMetamask} type="button">
|
||||
<img className="h-5 mr-1" src="/integrations/metamask.svg" />
|
||||
{t("connect_metamask")}
|
||||
</Button>
|
||||
);
|
||||
}, [connectMetamask, t]);
|
||||
|
||||
const oneStepButton = useMemo(() => {
|
||||
return (
|
||||
<Button
|
||||
color="secondary"
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
await connectMetamask();
|
||||
await verifyWallet();
|
||||
}}>
|
||||
<img className="h-5 mr-1" src="/integrations/metamask.svg" />
|
||||
{t("verify_wallet")}
|
||||
</Button>
|
||||
);
|
||||
}, [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 (
|
||||
<div
|
||||
className="absolute transition-opacity transform -translate-x-1/2 -translate-y-1/2 opacity-0 top-1/2 left-1/2 group-hover:opacity-100"
|
||||
id={`crypto-${props.id}`}>
|
||||
{determineButton()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CryptoSection;
|
|
@ -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[]) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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<typeof getServerSideProps>) {
|
||||
const { isReady } = useTheme(props.user.theme);
|
||||
const { user, eventTypes } = props;
|
||||
|
@ -26,6 +34,8 @@ export default function User(props: inferSSRProps<typeof getServerSideProps>) {
|
|||
|
||||
const nameOrUsername = user.name || user.username || "";
|
||||
|
||||
const [evtsToVerify, setEvtsToVerify] = useState<EvtsToVerify>({});
|
||||
|
||||
return (
|
||||
<>
|
||||
<HeadSeo
|
||||
|
@ -50,17 +60,11 @@ export default function User(props: inferSSRProps<typeof getServerSideProps>) {
|
|||
<p className="text-neutral-500 dark:text-white">{user.bio}</p>
|
||||
</div>
|
||||
<div className="space-y-6" data-testid="event-types">
|
||||
{user.away && (
|
||||
<div className="relative px-6 py-4 bg-white border rounded-sm group dark:bg-neutral-900 dark:border-0 border-neutral-200">
|
||||
<MoonIcon className="w-8 h-8 mb-4 text-neutral-800" />
|
||||
<h2 className="font-semibold text-neutral-900 dark:text-white">{t("user_away")}</h2>
|
||||
<p className="text-neutral-500 dark:text-white">{t("user_away_description")}</p>
|
||||
</div>
|
||||
)}
|
||||
{!user.away &&
|
||||
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
|
||||
|
@ -68,11 +72,33 @@ 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.metadata.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.isWeb3Active && type.metadata.smartContractAddress && (
|
||||
<CryptoSection
|
||||
id={type.id}
|
||||
pathname={`/${user.username}/${type.slug}`}
|
||||
smartContractAddress={type.metadata.smartContractAddress as string}
|
||||
verified={evtsToVerify[type.id]}
|
||||
setEvtsToVerify={setEvtsToVerify}
|
||||
oneStep
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
@ -87,6 +113,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps>) {
|
|||
</div>
|
||||
)}
|
||||
</main>
|
||||
<Toaster position="bottom-right" />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
@ -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: {
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 (
|
||||
<AppProviders {...props}>
|
||||
<DefaultSeo {...seoConfig.defaultNextSeo} />
|
||||
<I18nLanguageHandler />
|
||||
<Component {...pageProps} err={err} />
|
||||
</AppProviders>
|
||||
<ContractsProvider>
|
||||
<AppProviders {...props}>
|
||||
<DefaultSeo {...seoConfig.defaultNextSeo} />
|
||||
<I18nLanguageHandler />
|
||||
<Component {...pageProps} err={err} />
|
||||
</AppProviders>
|
||||
</ContractsProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<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 });
|
||||
|
|
|
@ -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<Token>;
|
||||
}
|
||||
type AvailabilityInput = Pick<Availability, "days" | "startTime" | "endTime">;
|
||||
|
||||
type OptionTypeBase = {
|
||||
|
@ -145,6 +158,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) ||
|
||||
|
@ -153,6 +167,41 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
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<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 || "");
|
||||
}, []);
|
||||
|
@ -261,6 +310,8 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
|
||||
const formMethods = useForm<{
|
||||
title: string;
|
||||
eventTitle: string;
|
||||
smartContractAddress: string;
|
||||
eventName: string;
|
||||
slug: string;
|
||||
length: number;
|
||||
|
@ -512,9 +563,13 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
<Form
|
||||
form={formMethods}
|
||||
handleSubmit={async (values) => {
|
||||
const { periodDates, periodCountCalendarDays, ...input } = values;
|
||||
const { periodDates, periodCountCalendarDays, smartContractAddress, ...input } = values;
|
||||
const metadata = {
|
||||
smartContractAddress: smartContractAddress,
|
||||
};
|
||||
updateMutation.mutate({
|
||||
...input,
|
||||
metadata,
|
||||
periodStartDate: periodDates.startDate,
|
||||
periodEndDate: periodDates.endDate,
|
||||
periodCountCalendarDays: periodCountCalendarDays === "1",
|
||||
|
@ -728,6 +783,30 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{eventType.isWeb3Active && (
|
||||
<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.metadata.smartContractAddress || "") as string}
|
||||
{...formMethods.register("smartContractAddress")}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="items-center block sm:flex">
|
||||
<div className="mb-4 min-w-48 sm:mb-0">
|
||||
<label
|
||||
|
@ -1385,6 +1464,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||
customInputs: true,
|
||||
timeZone: true,
|
||||
periodType: true,
|
||||
metadata: true,
|
||||
periodDays: true,
|
||||
periodStartDate: true,
|
||||
periodEndDate: true,
|
||||
|
@ -1426,10 +1506,27 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||
address?: string;
|
||||
};
|
||||
|
||||
const { locations, ...restEventType } = rawEventType;
|
||||
const credentials = await prisma.credential.findMany({
|
||||
where: {
|
||||
userId: session.user.id,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
type: true,
|
||||
key: true,
|
||||
},
|
||||
});
|
||||
|
||||
const web3Credentials = credentials.find((credential) => credential.type.includes("_web3"));
|
||||
const { locations, metadata, ...restEventType } = rawEventType;
|
||||
const eventType = {
|
||||
...restEventType,
|
||||
locations: locations as unknown as Location[],
|
||||
metadata: (metadata || {}) as JSONObject,
|
||||
isWeb3Active:
|
||||
web3Credentials && web3Credentials.key
|
||||
? (((web3Credentials.key as JSONObject).isWeb3Active || false) as boolean)
|
||||
: false,
|
||||
};
|
||||
|
||||
// backwards compat
|
||||
|
@ -1444,17 +1541,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||
eventType.users.push(fallbackUser);
|
||||
}
|
||||
|
||||
const credentials = await prisma.credential.findMany({
|
||||
where: {
|
||||
userId: session.user.id,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
type: true,
|
||||
key: true,
|
||||
},
|
||||
});
|
||||
|
||||
const integrations = getIntegrations(credentials);
|
||||
|
||||
const locationOptions: OptionTypeBase[] = [];
|
||||
|
|
|
@ -2,11 +2,13 @@ import { ChevronRightIcon, PencilAltIcon, SwitchHorizontalIcon, TrashIcon } from
|
|||
import { ClipboardIcon } from "@heroicons/react/solid";
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible";
|
||||
import Image from "next/image";
|
||||
import React, { useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
||||
import { JSONObject } from "superjson/dist/types";
|
||||
|
||||
import { QueryCell } from "@lib/QueryCell";
|
||||
import classNames from "@lib/classNames";
|
||||
import { HttpError } from "@lib/core/http/error";
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
import showToast from "@lib/notification";
|
||||
import { inferQueryOutput, trpc } from "@lib/trpc";
|
||||
|
@ -543,6 +545,86 @@ 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 (
|
||||
<a className="text-blue-500" target="_blank" href="https://cal.com/web3" rel="noreferrer">
|
||||
Read more
|
||||
</a>
|
||||
)
|
||||
</ListItemTitle>
|
||||
<ListItemText component="p">{t("only_book_people_and_allow")}</ListItemText>
|
||||
</div>
|
||||
<Web3ConnectBtn />
|
||||
</div>
|
||||
</ListItem>
|
||||
</List>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Web3ConnectBtn() {
|
||||
const { t } = useLocale();
|
||||
const utils = trpc.useContext();
|
||||
const [connectionBtn, setConnection] = useState(false);
|
||||
const result = trpc.useQuery(["viewer.web3Integration"]);
|
||||
const mutation = trpc.useMutation("viewer.enableOrDisableWeb3", {
|
||||
onSuccess: async (result) => {
|
||||
const { key = {} } = result as JSONObject;
|
||||
|
||||
if ((key as JSONObject).isWeb3Active) {
|
||||
showToast(t("web3_metamask_added"), "success");
|
||||
} else {
|
||||
showToast(t("web3_metamask_disconnected"), "success");
|
||||
}
|
||||
},
|
||||
onError: (err) => {
|
||||
if (err instanceof HttpError) {
|
||||
const message = `${err.statusCode}: ${err.message}`;
|
||||
showToast(message, "error");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (result.data) {
|
||||
setConnection(result.data.isWeb3Active as boolean);
|
||||
}
|
||||
}, [result]);
|
||||
|
||||
const enableOrDisableWeb3 = async (mutation: any) => {
|
||||
const result = await mutation.mutateAsync({});
|
||||
setConnection(result.key.isWeb3Active);
|
||||
utils.invalidateQueries("viewer.web3Integration");
|
||||
};
|
||||
|
||||
if (mutation.isLoading) {
|
||||
return <Loader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
color={connectionBtn ? "warn" : "secondary"}
|
||||
disabled={result.isLoading || mutation.isLoading}
|
||||
onClick={async () => await enableOrDisableWeb3(mutation)}
|
||||
data-testid="metamask">
|
||||
{connectionBtn ? t("remove") : t("add")}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export default function IntegrationsPage() {
|
||||
const { t } = useLocale();
|
||||
|
||||
|
@ -553,6 +635,7 @@ export default function IntegrationsPage() {
|
|||
<CalendarListContainer />
|
||||
<WebhookListContainer />
|
||||
<IframeEmbedContainer />
|
||||
<Web3Container />
|
||||
</ClientSuspense>
|
||||
</Shell>
|
||||
);
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { GetServerSidePropsContext } from "next";
|
||||
import { JSONObject } from "superjson/dist/types";
|
||||
|
||||
import { asStringOrNull } from "@lib/asStringOrNull";
|
||||
import { getWorkingHours } from "@lib/availability";
|
||||
|
@ -64,6 +65,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||
currency: true,
|
||||
timeZone: true,
|
||||
slotInterval: true,
|
||||
metadata: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -85,6 +87,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,
|
||||
});
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { GetServerSidePropsContext } from "next";
|
||||
import { JSONObject } from "superjson/dist/types";
|
||||
|
||||
import { asStringOrThrow } from "@lib/asStringOrNull";
|
||||
import prisma from "@lib/prisma";
|
||||
|
@ -40,6 +41,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||
disableGuests: true,
|
||||
price: true,
|
||||
currency: true,
|
||||
metadata: true,
|
||||
team: {
|
||||
select: {
|
||||
slug: true,
|
||||
|
@ -61,6 +63,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||
const eventTypeObject = [eventType].map((e) => {
|
||||
return {
|
||||
...e,
|
||||
metadata: (eventType.metadata || {}) as JSONObject,
|
||||
periodStartDate: e.periodStartDate?.toString() ?? null,
|
||||
periodEndDate: e.periodEndDate?.toString() ?? null,
|
||||
};
|
||||
|
|
38
prisma/migrations/20211222174947_placeholder/migration.sql
Normal file
38
prisma/migrations/20211222174947_placeholder/migration.sql
Normal 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";
|
|
@ -0,0 +1,2 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "EventType" ADD COLUMN "scAddress" TEXT;
|
|
@ -0,0 +1,3 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "EventType" DROP COLUMN "scAddress",
|
||||
ADD COLUMN "smartContractAddress" TEXT;
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE "EventType" ADD COLUMN "metadata" JSONB;
|
|
@ -63,6 +63,7 @@ model EventType {
|
|||
price Int @default(0)
|
||||
currency String @default("usd")
|
||||
slotInterval Int?
|
||||
metadata Json?
|
||||
|
||||
@@unique([userId, slug])
|
||||
}
|
||||
|
@ -216,7 +217,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 {
|
||||
|
|
|
@ -52,6 +52,7 @@ export const _EventTypeModel = z.object({
|
|||
price: z.number().int(),
|
||||
currency: z.string(),
|
||||
slotInterval: z.number().int().nullish(),
|
||||
metadata: jsonSchema,
|
||||
});
|
||||
|
||||
export interface CompleteEventType extends z.infer<typeof _EventTypeModel> {
|
||||
|
|
61
public/integrations/metamask.svg
Normal file
61
public/integrations/metamask.svg
Normal 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 |
5
public/integrations/web3.svg
Normal file
5
public/integrations/web3.svg
Normal 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 |
|
@ -468,6 +468,8 @@
|
|||
"event_type_created_successfully": "{{eventTypeTitle}} Ereignistyp erfolgreich erstellt",
|
||||
"event_type_updated_successfully": "Ereignistyp erfolgreich aktualisiert",
|
||||
"event_type_deleted_successfully": "Event-Typ erfolgreich gelöscht",
|
||||
"web3_metamask_added": "Metamask added successfully",
|
||||
"web3_metamask_disconnected": "Metamask disconnected successfully",
|
||||
"hours": "Stunden",
|
||||
"your_email": "Ihre Emailadresse",
|
||||
"change_avatar": "Profilbild ändern",
|
||||
|
|
|
@ -468,6 +468,8 @@
|
|||
"event_type_created_successfully": "{{eventTypeTitle}} event type created successfully",
|
||||
"event_type_updated_successfully": "{{eventTypeTitle}} event type updated successfully",
|
||||
"event_type_deleted_successfully": "Event type deleted successfully",
|
||||
"web3_metamask_added": "Metamask added successfully",
|
||||
"web3_metamask_disconnected": "Metamask disconnected successfully",
|
||||
"hours": "Hours",
|
||||
"your_email": "Your Email",
|
||||
"change_avatar": "Change Avatar",
|
||||
|
@ -583,7 +585,8 @@
|
|||
"set_as_free": "Disable away status",
|
||||
"user_away": "This user is currently away.",
|
||||
"user_away_description": "The person you are trying to book has set themselves to away, and therefore is not accepting new bookings.",
|
||||
"saml_config_updated_successfully": "SAML configuration updated successfully",
|
||||
"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.",
|
||||
"saml_config_deleted_successfully": "SAML configuration deleted successfully",
|
||||
"account_created_with_identity_provider": "Your account was created using an Identity Provider.",
|
||||
"account_managed_by_identity_provider": "Your account is managed by {{provider}}",
|
||||
|
@ -604,5 +607,9 @@
|
|||
"import": "Import",
|
||||
"import_from": "Import from",
|
||||
"access_token": "Access token",
|
||||
"visit_roadmap": "Roadmap"
|
||||
"visit_roadmap": "Roadmap",
|
||||
"remove": "Remove",
|
||||
"add": "Add",
|
||||
"verify_wallet": "Verify wallet",
|
||||
"connect_metamask": "Connect Metamask"
|
||||
}
|
||||
|
|
|
@ -458,6 +458,8 @@
|
|||
"event_type_created_successfully": "{{eventTypeTitle}} tipo de evento creado con éxito",
|
||||
"event_type_updated_successfully": "{{eventTypeTitle}} tipo de evento actualizado con éxito",
|
||||
"event_type_deleted_successfully": "Tipo de evento eliminado con éxito",
|
||||
"web3_metamask_added": "Metamask ha sido añadido exitosamente",
|
||||
"web3_metamask_disconnected": "Metamask ha sido removido exitosamente",
|
||||
"hours": "Horas",
|
||||
"your_email": "Tu Email",
|
||||
"change_avatar": "Cambiar Avatar",
|
||||
|
|
|
@ -426,6 +426,8 @@
|
|||
"event_type_created_successfully": "Type d'événement {{eventTypeTitle}} créé avec succès",
|
||||
"event_type_updated_successfully": "Type d'événement {{eventTypeTitle}} mis à jour avec succès",
|
||||
"event_type_deleted_successfully": "Type d'événement supprimé avec succès",
|
||||
"web3_metamask_added": "Metamask added successfully",
|
||||
"web3_metamask_disconnected": "Metamask disconnected successfully",
|
||||
"hours": "Heures",
|
||||
"your_email": "Votre adresse e-mail",
|
||||
"change_avatar": "Changer d'avatar",
|
||||
|
|
|
@ -452,6 +452,8 @@
|
|||
"event_type_created_successfully": "{{eventTypeTitle}} tipo di evento creato con successo",
|
||||
"event_type_updated_successfully": "{{eventTypeTitle}} tipo di evento aggiornato con successo",
|
||||
"event_type_deleted_successfully": "Tipo di evento eliminato con successo",
|
||||
"web3_metamask_added": "Metamask added successfully",
|
||||
"web3_metamask_disconnected": "Metamask disconnected successfully",
|
||||
"hours": "Ore",
|
||||
"your_email": "La Tua Email",
|
||||
"change_avatar": "Cambia Avatar",
|
||||
|
|
|
@ -425,6 +425,8 @@
|
|||
"event_type_created_successfully": "{{eventTypeTitle}} イベント種別が正常に作成されました",
|
||||
"event_type_updated_successfully": "{{eventTypeTitle}} イベント種別が正常に更新されました",
|
||||
"event_type_deleted_successfully": "イベント種別が正常に削除されました",
|
||||
"web3_metamask_added": "Metamask added successfully",
|
||||
"web3_metamask_disconnected": "Metamask disconnected successfully",
|
||||
"hours": "時間",
|
||||
"your_email": "あなたのメールアドレス",
|
||||
"change_avatar": "アバターを変更",
|
||||
|
|
|
@ -447,6 +447,8 @@
|
|||
"event_type_created_successfully": "{{eventTypeTitle}} 이벤트 타입이 성공적으로 생성되었습니다.",
|
||||
"event_type_updated_successfully": "{{eventTypeTitle}} 이벤트 타입이 성공적으로 업데이트되었습니다.",
|
||||
"event_type_deleted_successfully": "이벤트 타입이 성공적으로 삭제되었습니다.",
|
||||
"web3_metamask_added": "Metamask added successfully",
|
||||
"web3_metamask_disconnected": "Metamask disconnected successfully",
|
||||
"hours": "시간",
|
||||
"your_email": "이메일 주소",
|
||||
"change_avatar": "아바타 변경하기",
|
||||
|
|
|
@ -418,6 +418,8 @@
|
|||
"event_type_created_successfully": "{{eventTypeTitle}} evenement met succes aangemaakt",
|
||||
"event_type_updated_successfully": "Evenement is bijgewerkt",
|
||||
"event_type_deleted_successfully": "Evenement is verwijderd",
|
||||
"web3_metamask_added": "Metamask added successfully",
|
||||
"web3_metamask_disconnected": "Metamask disconnected successfully",
|
||||
"hours": "Uren",
|
||||
"your_email": "Uw E-mailadres",
|
||||
"change_avatar": "Wijzig avatar",
|
||||
|
|
|
@ -462,6 +462,8 @@
|
|||
"event_type_created_successfully": "Utworzono {{eventTypeTitle}} typ wydarzenia pomyślnie",
|
||||
"event_type_updated_successfully": "{{eventTypeTitle}} typ wydarzenia został pomyślnie zaktualizowany",
|
||||
"event_type_deleted_successfully": "Typ wydarzenia usunięty pomyślnie",
|
||||
"web3_metamask_added": "Metamask added successfully",
|
||||
"web3_metamask_disconnected": "Metamask disconnected successfully",
|
||||
"hours": "Godziny",
|
||||
"your_email": "Twój e-mail",
|
||||
"change_avatar": "Zmień Awatar",
|
||||
|
|
|
@ -462,6 +462,8 @@
|
|||
"event_type_created_successfully": "{{eventTypeTitle}} evento criado com sucesso",
|
||||
"event_type_updated_successfully": "Tipo de evento atualizado com sucesso",
|
||||
"event_type_deleted_successfully": "Tipo de evento removido com sucesso",
|
||||
"web3_metamask_added": "Metamask added successfully",
|
||||
"web3_metamask_disconnected": "Metamask disconnected successfully",
|
||||
"hours": "Horas",
|
||||
"your_email": "Seu Email",
|
||||
"change_avatar": "Alterar Avatar",
|
||||
|
|
|
@ -468,6 +468,8 @@
|
|||
"event_type_created_successfully": "Tipo de evento {{eventTypeTitle}} criado com sucesso",
|
||||
"event_type_updated_successfully": "Tipo de evento atualizado com sucesso",
|
||||
"event_type_deleted_successfully": "Tipo de evento eliminado com sucesso",
|
||||
"web3_metamask_added": "Metamask added successfully",
|
||||
"web3_metamask_disconnected": "Metamask disconnected successfully",
|
||||
"hours": "Horas",
|
||||
"your_email": "Seu Email",
|
||||
"change_avatar": "Alterar Avatar",
|
||||
|
|
|
@ -425,6 +425,8 @@
|
|||
"event_type_created_successfully": "{{eventTypeTitle}} tip de eveniment creat cu succes",
|
||||
"event_type_updated_successfully": "{{eventTypeTitle}} tip de eveniment actualizat cu succes",
|
||||
"event_type_deleted_successfully": "Tipul de eveniment șters cu succes",
|
||||
"web3_metamask_added": "Metamask added successfully",
|
||||
"web3_metamask_disconnected": "Metamask disconnected successfully",
|
||||
"hours": "Ore",
|
||||
"your_email": "E-mailul tău",
|
||||
"change_avatar": "Schimbă avatarul",
|
||||
|
|
|
@ -462,6 +462,8 @@
|
|||
"event_type_created_successfully": "{{eventTypeTitle}} тип мероприятия успешно создан",
|
||||
"event_type_updated_successfully": "Шаблон события успешно обновлён",
|
||||
"event_type_deleted_successfully": "Тип события успешно удален",
|
||||
"web3_metamask_added": "Metamask added successfully",
|
||||
"web3_metamask_disconnected": "Metamask disconnected successfully",
|
||||
"hours": "Часы",
|
||||
"your_email": "Ваш адрес электронной почты",
|
||||
"change_avatar": "Изменить аватар",
|
||||
|
|
|
@ -468,6 +468,8 @@
|
|||
"event_type_created_successfully": "{{eventTypeTitle}} 事件类型创建成功",
|
||||
"event_type_updated_successfully": "{{eventTypeTitle}} 事件类型更新成功",
|
||||
"event_type_deleted_successfully": "事件类型删除成功",
|
||||
"web3_metamask_added": "Metamask added successfully",
|
||||
"web3_metamask_disconnected": "Metamask disconnected successfully",
|
||||
"hours": "小时数",
|
||||
"your_email": "您的邮箱",
|
||||
"change_avatar": "修改头像",
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { BookingStatus, MembershipRole, Prisma } from "@prisma/client";
|
||||
import _ from "lodash";
|
||||
import { JSONObject } from "superjson/dist/types";
|
||||
import { z } from "zod";
|
||||
|
||||
import { checkPremiumUsername } from "@ee/lib/core/checkPremiumUsername";
|
||||
|
@ -463,6 +464,44 @@ const loggedInViewerRouter = createProtectedRouter()
|
|||
});
|
||||
},
|
||||
})
|
||||
.mutation("enableOrDisableWeb3", {
|
||||
input: z.object({}),
|
||||
async resolve({ ctx }) {
|
||||
const { user } = ctx;
|
||||
const where = { userId: user.id, type: "metamask_web3" };
|
||||
|
||||
const web3Credential = await ctx.prisma.credential.findFirst({
|
||||
where,
|
||||
select: {
|
||||
id: true,
|
||||
key: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (web3Credential) {
|
||||
return ctx.prisma.credential.update({
|
||||
where: {
|
||||
id: web3Credential.id,
|
||||
},
|
||||
data: {
|
||||
key: {
|
||||
isWeb3Active: !(web3Credential.key as JSONObject).isWeb3Active,
|
||||
},
|
||||
},
|
||||
});
|
||||
} else {
|
||||
return ctx.prisma.credential.create({
|
||||
data: {
|
||||
type: "metamask_web3",
|
||||
key: {
|
||||
isWeb3Active: true,
|
||||
} as unknown as Prisma.InputJsonObject,
|
||||
userId: user.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
})
|
||||
.query("integrations", {
|
||||
async resolve({ ctx }) {
|
||||
const { user } = ctx;
|
||||
|
@ -498,6 +537,24 @@ const loggedInViewerRouter = createProtectedRouter()
|
|||
};
|
||||
},
|
||||
})
|
||||
.query("web3Integration", {
|
||||
async resolve({ ctx }) {
|
||||
const { user } = ctx;
|
||||
|
||||
const where = { userId: user.id, type: "metamask_web3" };
|
||||
|
||||
const web3Credential = await ctx.prisma.credential.findFirst({
|
||||
where,
|
||||
select: {
|
||||
key: true,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
isWeb3Active: web3Credential ? (web3Credential.key as JSONObject).isWeb3Active : false,
|
||||
};
|
||||
},
|
||||
})
|
||||
.query("availability", {
|
||||
async resolve({ ctx }) {
|
||||
const { prisma, user } = ctx;
|
||||
|
|
1
tsconfig.tsbuildinfo
Normal file
1
tsconfig.tsbuildinfo
Normal file
File diff suppressed because one or more lines are too long
9
web3/abis/abiWithGetBalance.json
Normal file
9
web3/abis/abiWithGetBalance.json
Normal 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
7004
web3/dummyResps/bloxyApi.js
Normal file
File diff suppressed because it is too large
Load diff
12
web3/utils/verifyAccount.ts
Normal file
12
web3/utils/verifyAccount.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import Web3 from "web3";
|
||||
|
||||
export const AUTH_MESSAGE =
|
||||
"I authorize the use of my Ethereum address for the purposes of this application.";
|
||||
|
||||
const verifyAccount = async (signature: string, address: string) => {
|
||||
const web3 = new Web3();
|
||||
const signingAddress = await web3.eth.accounts.recover(AUTH_MESSAGE, signature);
|
||||
if (!(address.toLowerCase() === signingAddress.toLowerCase())) throw new Error("Failed to verify address");
|
||||
};
|
||||
|
||||
export default verifyAccount;
|
Loading…
Reference in a new issue