diff --git a/packages/nextjs/app/blockexplorer/_components/AddressCodeTab.tsx b/packages/nextjs/app/blockexplorer/_components/AddressCodeTab.tsx old mode 100644 new mode 100755 diff --git a/packages/nextjs/app/blockexplorer/_components/AddressComponent.tsx b/packages/nextjs/app/blockexplorer/_components/AddressComponent.tsx old mode 100644 new mode 100755 diff --git a/packages/nextjs/app/blockexplorer/_components/AddressLogsTab.tsx b/packages/nextjs/app/blockexplorer/_components/AddressLogsTab.tsx old mode 100644 new mode 100755 diff --git a/packages/nextjs/app/blockexplorer/_components/AddressStorageTab.tsx b/packages/nextjs/app/blockexplorer/_components/AddressStorageTab.tsx old mode 100644 new mode 100755 diff --git a/packages/nextjs/app/blockexplorer/_components/BackButton.tsx b/packages/nextjs/app/blockexplorer/_components/BackButton.tsx old mode 100644 new mode 100755 diff --git a/packages/nextjs/app/blockexplorer/_components/ContractTabs.tsx b/packages/nextjs/app/blockexplorer/_components/ContractTabs.tsx old mode 100644 new mode 100755 diff --git a/packages/nextjs/app/blockexplorer/_components/PaginationButton.tsx b/packages/nextjs/app/blockexplorer/_components/PaginationButton.tsx old mode 100644 new mode 100755 diff --git a/packages/nextjs/app/blockexplorer/_components/SearchBar.tsx b/packages/nextjs/app/blockexplorer/_components/SearchBar.tsx old mode 100644 new mode 100755 diff --git a/packages/nextjs/app/blockexplorer/_components/TransactionHash.tsx b/packages/nextjs/app/blockexplorer/_components/TransactionHash.tsx old mode 100644 new mode 100755 diff --git a/packages/nextjs/app/blockexplorer/_components/TransactionsTable.tsx b/packages/nextjs/app/blockexplorer/_components/TransactionsTable.tsx old mode 100644 new mode 100755 diff --git a/packages/nextjs/app/blockexplorer/_components/index.tsx b/packages/nextjs/app/blockexplorer/_components/index.tsx old mode 100644 new mode 100755 diff --git a/packages/nextjs/app/blockexplorer/address/[address]/page.tsx b/packages/nextjs/app/blockexplorer/address/[address]/page.tsx old mode 100644 new mode 100755 diff --git a/packages/nextjs/app/blockexplorer/layout.tsx b/packages/nextjs/app/blockexplorer/layout.tsx old mode 100644 new mode 100755 diff --git a/packages/nextjs/app/blockexplorer/page.tsx b/packages/nextjs/app/blockexplorer/page.tsx old mode 100644 new mode 100755 diff --git a/packages/nextjs/app/blockexplorer/transaction/[txHash]/page.tsx b/packages/nextjs/app/blockexplorer/transaction/[txHash]/page.tsx old mode 100644 new mode 100755 diff --git a/packages/nextjs/app/blockexplorer/transaction/_components/TransactionComp.tsx b/packages/nextjs/app/blockexplorer/transaction/_components/TransactionComp.tsx old mode 100644 new mode 100755 diff --git a/packages/nextjs/app/config/index.ts b/packages/nextjs/app/config/index.ts new file mode 100755 index 0000000..6f44024 --- /dev/null +++ b/packages/nextjs/app/config/index.ts @@ -0,0 +1 @@ +export { defaultSnapOrigin } from './snap'; diff --git a/packages/nextjs/app/config/snap.ts b/packages/nextjs/app/config/snap.ts new file mode 100755 index 0000000..d17b20b --- /dev/null +++ b/packages/nextjs/app/config/snap.ts @@ -0,0 +1,11 @@ +/** + * The snap origin to use. + * Will default to the local hosted snap if no value is provided in environment. + * + * You may be tempted to change this to the URL where your production snap is hosted, but please + * don't. Instead, rename `.env.production.dist` to `.env.production` and set the production URL + * there. Running `yarn build` will automatically use the production environment variables. + */ +export const defaultSnapOrigin = + // eslint-disable-next-line no-restricted-globals + process.env.SNAP_ORIGIN ?? `local:http://localhost:8080`; diff --git a/packages/nextjs/app/config/theme.ts b/packages/nextjs/app/config/theme.ts new file mode 100755 index 0000000..cd0cc2a --- /dev/null +++ b/packages/nextjs/app/config/theme.ts @@ -0,0 +1,187 @@ +import type { DefaultTheme } from 'styled-components'; +import { createGlobalStyle } from 'styled-components'; + +const breakpoints = ['600px', '768px', '992px']; + +/** + * Common theme properties. + */ +const theme = { + fonts: { + default: + '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif', + code: 'ui-monospace,Menlo,Monaco,"Cascadia Mono","Segoe UI Mono","Roboto Mono","Oxygen Mono","Ubuntu Monospace","Source Code Pro","Fira Mono","Droid Sans Mono","Courier New", monospace', + }, + fontSizes: { + heading: '5.2rem', + mobileHeading: '3.6rem', + title: '2.4rem', + large: '2rem', + text: '1.6rem', + small: '1.4rem', + }, + radii: { + default: '24px', + button: '8px', + }, + breakpoints, + mediaQueries: { + small: `@media screen and (max-width: ${breakpoints[0] as string})`, + medium: `@media screen and (min-width: ${breakpoints[1] as string})`, + large: `@media screen and (min-width: ${breakpoints[2] as string})`, + }, + shadows: { + default: '0px 7px 42px rgba(0, 0, 0, 0.1)', + button: '0px 0px 16.1786px rgba(0, 0, 0, 0.15);', + }, +}; + +/** + * Light theme color properties. + */ +export const light: DefaultTheme = { + colors: { + background: { + default: '#FFFFFF', + alternative: '#F2F4F6', + inverse: '#141618', + }, + icon: { + default: '#141618', + alternative: '#BBC0C5', + }, + text: { + default: '#24272A', + muted: '#6A737D', + alternative: '#535A61', + inverse: '#FFFFFF', + }, + border: { + default: '#BBC0C5', + }, + primary: { + default: '#6F4CFF', + inverse: '#FFFFFF', + }, + card: { + default: '#FFFFFF', + }, + error: { + default: '#d73a49', + alternative: '#b92534', + muted: '#d73a4919', + }, + }, + ...theme, +}; + +/** + * Dark theme color properties + */ +export const dark: DefaultTheme = { + colors: { + background: { + default: '#24272A', + alternative: '#141618', + inverse: '#FFFFFF', + }, + icon: { + default: '#FFFFFF', + alternative: '#BBC0C5', + }, + text: { + default: '#FFFFFF', + muted: '#FFFFFF', + alternative: '#D6D9DC', + inverse: '#24272A', + }, + border: { + default: '#848C96', + }, + primary: { + default: '#6F4CFF', + inverse: '#FFFFFF', + }, + card: { + default: '#141618', + }, + error: { + default: '#d73a49', + alternative: '#b92534', + muted: '#d73a4919', + }, + }, + ...theme, +}; + +/** + * Default style applied to the app. + * + * @param props - Styled Components props. + * @returns Global style React component. + */ +export const GlobalStyle = createGlobalStyle` + html { + /* 62.5% of the base size of 16px = 10px.*/ + font-size: 62.5%; + } + + body { + background-color: ${(props) => props.theme.colors.background?.default}; + color: ${(props) => props.theme.colors.text?.default}; + font-family: ${(props) => props.theme.fonts.default}; + font-size: ${(props) => props.theme.fontSizes.text}; + margin: 0; + } + + * { + transition: background-color .1s linear; + } + + h1, h2, h3, h4, h5, h6 { + font-size: ${(props) => props.theme.fontSizes.heading}; + ${(props) => props.theme.mediaQueries.small} { + font-size: ${(props) => props.theme.fontSizes.mobileHeading}; + } + } + + code { + background-color: ${(props) => props.theme.colors.background?.alternative}; + font-family: ${(props) => props.theme.fonts.code}; + padding: 1.2rem; + font-weight: normal; + font-size: ${(props) => props.theme.fontSizes.text}; + } + + button { + font-size: ${(props) => props.theme.fontSizes.small}; + border-radius: ${(props) => props.theme.radii.button}; + background-color: ${(props) => props.theme.colors.background?.inverse}; + color: ${(props) => props.theme.colors.text?.inverse}; + border: 1px solid ${(props) => props.theme.colors.background?.inverse}; + font-weight: bold; + padding: 1rem; + min-height: 4.2rem; + cursor: pointer; + transition: all .2s ease-in-out; + + &:hover { + background-color: transparent; + border: 1px solid ${(props) => props.theme.colors.background?.inverse}; + color: ${(props) => props.theme.colors.text?.default}; + } + + &:disabled, + &[disabled] { + border: 1px solid ${(props) => props.theme.colors.background?.inverse}; + cursor: not-allowed; + } + + &:disabled:hover, + &[disabled]:hover { + background-color: ${(props) => props.theme.colors.background?.inverse}; + color: ${(props) => props.theme.colors.text?.inverse}; + border: 1px solid ${(props) => props.theme.colors.background?.inverse}; + } + } +`; diff --git a/packages/nextjs/app/create/page.tsx b/packages/nextjs/app/create/page.tsx old mode 100644 new mode 100755 diff --git a/packages/nextjs/app/debug/_components/DebugContracts.tsx b/packages/nextjs/app/debug/_components/DebugContracts.tsx old mode 100644 new mode 100755 diff --git a/packages/nextjs/app/debug/_components/contract/ContractInput.tsx b/packages/nextjs/app/debug/_components/contract/ContractInput.tsx old mode 100644 new mode 100755 diff --git a/packages/nextjs/app/debug/_components/contract/ContractReadMethods.tsx b/packages/nextjs/app/debug/_components/contract/ContractReadMethods.tsx old mode 100644 new mode 100755 diff --git a/packages/nextjs/app/debug/_components/contract/ContractUI.tsx b/packages/nextjs/app/debug/_components/contract/ContractUI.tsx old mode 100644 new mode 100755 diff --git a/packages/nextjs/app/debug/_components/contract/ContractVariables.tsx b/packages/nextjs/app/debug/_components/contract/ContractVariables.tsx old mode 100644 new mode 100755 diff --git a/packages/nextjs/app/debug/_components/contract/ContractWriteMethods.tsx b/packages/nextjs/app/debug/_components/contract/ContractWriteMethods.tsx old mode 100644 new mode 100755 diff --git a/packages/nextjs/app/debug/_components/contract/DisplayVariable.tsx b/packages/nextjs/app/debug/_components/contract/DisplayVariable.tsx old mode 100644 new mode 100755 diff --git a/packages/nextjs/app/debug/_components/contract/InheritanceTooltip.tsx b/packages/nextjs/app/debug/_components/contract/InheritanceTooltip.tsx old mode 100644 new mode 100755 diff --git a/packages/nextjs/app/debug/_components/contract/ReadOnlyFunctionForm.tsx b/packages/nextjs/app/debug/_components/contract/ReadOnlyFunctionForm.tsx old mode 100644 new mode 100755 diff --git a/packages/nextjs/app/debug/_components/contract/Tuple.tsx b/packages/nextjs/app/debug/_components/contract/Tuple.tsx old mode 100644 new mode 100755 diff --git a/packages/nextjs/app/debug/_components/contract/TupleArray.tsx b/packages/nextjs/app/debug/_components/contract/TupleArray.tsx old mode 100644 new mode 100755 diff --git a/packages/nextjs/app/debug/_components/contract/TxReceipt.tsx b/packages/nextjs/app/debug/_components/contract/TxReceipt.tsx old mode 100644 new mode 100755 diff --git a/packages/nextjs/app/debug/_components/contract/WriteOnlyFunctionForm.tsx b/packages/nextjs/app/debug/_components/contract/WriteOnlyFunctionForm.tsx old mode 100644 new mode 100755 diff --git a/packages/nextjs/app/debug/_components/contract/index.tsx b/packages/nextjs/app/debug/_components/contract/index.tsx old mode 100644 new mode 100755 diff --git a/packages/nextjs/app/debug/_components/contract/utilsContract.tsx b/packages/nextjs/app/debug/_components/contract/utilsContract.tsx old mode 100644 new mode 100755 diff --git a/packages/nextjs/app/debug/_components/contract/utilsDisplay.tsx b/packages/nextjs/app/debug/_components/contract/utilsDisplay.tsx old mode 100644 new mode 100755 diff --git a/packages/nextjs/app/debug/page.tsx b/packages/nextjs/app/debug/page.tsx old mode 100644 new mode 100755 diff --git a/packages/nextjs/app/hooks/MetamaskContext.tsx b/packages/nextjs/app/hooks/MetamaskContext.tsx new file mode 100755 index 0000000..3ec49ff --- /dev/null +++ b/packages/nextjs/app/hooks/MetamaskContext.tsx @@ -0,0 +1,74 @@ +import type { MetaMaskInpageProvider } from '@metamask/providers'; +import type { ReactNode } from 'react'; +import { createContext, useContext, useEffect, useState } from 'react'; + +import type { Snap } from '../types'; +import { getSnapsProvider } from '../utils'; + +type MetaMaskContextType = { + provider: MetaMaskInpageProvider | null; + installedSnap: Snap | null; + error: Error | null; + setInstalledSnap: (snap: Snap | null) => void; + setError: (error: Error) => void; +}; + +export const MetaMaskContext = createContext({ + provider: null, + installedSnap: null, + error: null, + setInstalledSnap: () => { + /* no-op */ + }, + setError: () => { + /* no-op */ + }, +}); + +/** + * MetaMask context provider to handle MetaMask and snap status. + * + * @param props - React Props. + * @param props.children - React component to be wrapped by the Provider. + * @returns JSX. + */ +export const MetaMaskProvider = ({ children }: { children: ReactNode }) => { + const [provider, setProvider] = useState(null); + const [installedSnap, setInstalledSnap] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + getSnapsProvider().then(setProvider).catch(console.error); + }, []); + + useEffect(() => { + if (error) { + const timeout = setTimeout(() => { + setError(null); + }, 10000); + + return () => { + clearTimeout(timeout); + }; + } + + return undefined; + }, [error]); + + return ( + + {children} + + ); +}; + +/** + * Utility hook to consume the MetaMask context. + * + * @returns The MetaMask context. + */ +export function useMetaMaskContext() { + return useContext(MetaMaskContext); +} diff --git a/packages/nextjs/app/hooks/index.ts b/packages/nextjs/app/hooks/index.ts new file mode 100755 index 0000000..b9f151e --- /dev/null +++ b/packages/nextjs/app/hooks/index.ts @@ -0,0 +1,5 @@ +export * from './MetamaskContext'; +export * from './useInvokeSnap'; +export * from './useMetaMask'; +export * from './useRequest'; +export * from './useRequestSnap'; diff --git a/packages/nextjs/app/hooks/useInvokeSnap.ts b/packages/nextjs/app/hooks/useInvokeSnap.ts new file mode 100755 index 0000000..c02bf6a --- /dev/null +++ b/packages/nextjs/app/hooks/useInvokeSnap.ts @@ -0,0 +1,37 @@ +import { defaultSnapOrigin } from '../config'; +import { useRequest } from './useRequest'; + +export type InvokeSnapParams = { + method: string; + params?: Record; +}; + +/** + * Utility hook to wrap the `wallet_invokeSnap` method. + * + * @param snapId - The Snap ID to invoke. Defaults to the snap ID specified in the + * config. + * @returns The invokeSnap wrapper method. + */ +export const useInvokeSnap = (snapId = defaultSnapOrigin) => { + const request = useRequest(); + + /** + * Invoke the requested Snap method. + * + * @param params - The invoke params. + * @param params.method - The method name. + * @param params.params - The method params. + * @returns The Snap response. + */ + const invokeSnap = async ({ method, params }: InvokeSnapParams) => + request({ + method: 'wallet_invokeSnap', + params: { + snapId, + request: params ? { method, params } : { method }, + }, + }); + + return invokeSnap; +}; diff --git a/packages/nextjs/app/hooks/useMetaMask.ts b/packages/nextjs/app/hooks/useMetaMask.ts new file mode 100755 index 0000000..cbb796e --- /dev/null +++ b/packages/nextjs/app/hooks/useMetaMask.ts @@ -0,0 +1,56 @@ +import { useEffect, useState } from 'react'; + +import { defaultSnapOrigin } from '../config'; +import type { GetSnapsResponse } from '../types'; +import { useMetaMaskContext } from './MetamaskContext'; +import { useRequest } from './useRequest'; + +/** + * A Hook to retrieve useful data from MetaMask. + * @returns The informations. + */ +export const useMetaMask = () => { + const { provider, setInstalledSnap, installedSnap } = useMetaMaskContext(); + const request = useRequest(); + + const [isFlask, setIsFlask] = useState(false); + + const snapsDetected = provider !== null; + + /** + * Detect if the version of MetaMask is Flask. + */ + const detectFlask = async () => { + const clientVersion = await request({ + method: 'web3_clientVersion', + }); + + const isFlaskDetected = (clientVersion as string[])?.includes('flask'); + + setIsFlask(isFlaskDetected); + }; + + /** + * Get the Snap informations from MetaMask. + */ + const getSnap = async () => { + const snaps = (await request({ + method: 'wallet_getSnaps', + })) as GetSnapsResponse; + + setInstalledSnap(snaps[defaultSnapOrigin] ?? null); + }; + + useEffect(() => { + const detect = async () => { + if (provider) { + await detectFlask(); + await getSnap(); + } + }; + + detect().catch(console.error); + }, [provider]); + + return { isFlask, snapsDetected, installedSnap, getSnap }; +}; diff --git a/packages/nextjs/app/hooks/useRequest.ts b/packages/nextjs/app/hooks/useRequest.ts new file mode 100755 index 0000000..68ee255 --- /dev/null +++ b/packages/nextjs/app/hooks/useRequest.ts @@ -0,0 +1,40 @@ +import type { RequestArguments } from '@metamask/providers'; + +import { useMetaMaskContext } from './MetamaskContext'; + +export type Request = (params: RequestArguments) => Promise; + +/** + * Utility hook to consume the provider `request` method with the available provider. + * + * @returns The `request` function. + */ +export const useRequest = () => { + const { provider, setError } = useMetaMaskContext(); + + /** + * `provider.request` wrapper. + * + * @param params - The request params. + * @param params.method - The method to call. + * @param params.params - The method params. + * @returns The result of the request. + */ + const request: Request = async ({ method, params }) => { + try { + const data = + (await provider?.request({ + method, + params, + } as RequestArguments)) ?? null; + + return data; + } catch (requestError: any) { + setError(requestError); + + return null; + } + }; + + return request; +}; diff --git a/packages/nextjs/app/hooks/useRequestSnap.ts b/packages/nextjs/app/hooks/useRequestSnap.ts new file mode 100755 index 0000000..b35c9a3 --- /dev/null +++ b/packages/nextjs/app/hooks/useRequestSnap.ts @@ -0,0 +1,37 @@ +import { defaultSnapOrigin } from '../config'; +import type { Snap } from '../types'; +import { useMetaMaskContext } from './MetamaskContext'; +import { useRequest } from './useRequest'; + +/** + * Utility hook to wrap the `wallet_requestSnaps` method. + * + * @param snapId - The requested Snap ID. Defaults to the snap ID specified in the + * config. + * @param version - The requested version. + * @returns The `wallet_requestSnaps` wrapper. + */ +export const useRequestSnap = ( + snapId = defaultSnapOrigin, + version?: string, +) => { + const request = useRequest(); + const { setInstalledSnap } = useMetaMaskContext(); + + /** + * Request the Snap. + */ + const requestSnap = async () => { + const snaps = (await request({ + method: 'wallet_requestSnaps', + params: { + [snapId]: version ? { version } : {}, + }, + })) as Record; + + // Updates the `installedSnap` context variable since we just installed the Snap. + setInstalledSnap(snaps?.[snapId] ?? null); + }; + + return requestSnap; +}; diff --git a/packages/nextjs/app/layout.tsx b/packages/nextjs/app/layout.tsx old mode 100644 new mode 100755 index ac9cbde..9ae3de7 --- a/packages/nextjs/app/layout.tsx +++ b/packages/nextjs/app/layout.tsx @@ -1,20 +1,21 @@ +"use client" + import "@rainbow-me/rainbowkit/styles.css"; import { ScaffoldEthAppWithProviders } from "~~/components/ScaffoldEthAppWithProviders"; import { ThemeProvider } from "~~/components/ThemeProvider"; import "~~/styles/globals.css"; import { getMetadata } from "~~/utils/scaffold-eth/getMetadata"; +import { MetaMaskProvider } from './hooks'; -export const metadata = getMetadata({ - title: "Oh Snap!", - description: "On-chain Community Notes using Metamask Snap", -}); const ScaffoldEthApp = ({ children }: { children: React.ReactNode }) => { return ( - {children} + + {children} + diff --git a/packages/nextjs/app/magic/page.tsx b/packages/nextjs/app/magic/page.tsx old mode 100644 new mode 100755 diff --git a/packages/nextjs/app/page.tsx b/packages/nextjs/app/page.tsx old mode 100644 new mode 100755 index 364c6a7..c6ea4a0 --- a/packages/nextjs/app/page.tsx +++ b/packages/nextjs/app/page.tsx @@ -1,13 +1,24 @@ "use client"; import Link from "next/link"; +// import ReactDOM from 'react-dom'; +import { useMetaMask, useRequestSnap } from "./hooks"; import type { NextPage } from "next"; +import Confetti from "react-confetti"; import { useAccount } from "wagmi"; import { MagnifyingGlassIcon, PlusIcon } from "@heroicons/react/24/outline"; import { Address } from "~~/components/scaffold-eth"; +// import type { ComponentProps, useState } from 'react'; +// import { ReactComponent as FlaskFox } from '../assets/flask_fox.svg'; + + + const Home: NextPage = () => { const { address: connectedAddress } = useAccount(); + const requestSnap = useRequestSnap(); + + // const [confettiVisible, setConfettiVisible] = useState(false); return ( <> @@ -28,6 +39,16 @@ const Home: NextPage = () => { // alt={fighters[0].name} // style={{ height: "200px" }} /> + + {/* here */} +
+ +



diff --git a/packages/nextjs/app/types/custom.d.ts b/packages/nextjs/app/types/custom.d.ts new file mode 100755 index 0000000..e909a80 --- /dev/null +++ b/packages/nextjs/app/types/custom.d.ts @@ -0,0 +1,25 @@ +import type { + EIP6963AnnounceProviderEvent, + EIP6963RequestProviderEvent, + MetaMaskInpageProvider, +} from '@metamask/providers'; + +/* + * Window type extension to support ethereum + */ +declare global { + // eslint-disable-next-line @typescript-eslint/consistent-type-definitions + interface Window { + ethereum: MetaMaskInpageProvider & { + setProvider?: (provider: MetaMaskInpageProvider) => void; + detected?: MetaMaskInpageProvider[]; + providers?: MetaMaskInpageProvider[]; + }; + } + + // eslint-disable-next-line @typescript-eslint/consistent-type-definitions + interface WindowEventMap { + 'eip6963:requestProvider': EIP6963RequestProviderEvent; + 'eip6963:announceProvider': EIP6963AnnounceProviderEvent; + } +} diff --git a/packages/nextjs/app/types/index.ts b/packages/nextjs/app/types/index.ts new file mode 100755 index 0000000..fb265f0 --- /dev/null +++ b/packages/nextjs/app/types/index.ts @@ -0,0 +1 @@ +export { type GetSnapsResponse, type Snap } from './snap'; diff --git a/packages/nextjs/app/types/snap.ts b/packages/nextjs/app/types/snap.ts new file mode 100755 index 0000000..8f96603 --- /dev/null +++ b/packages/nextjs/app/types/snap.ts @@ -0,0 +1,8 @@ +export type GetSnapsResponse = Record; + +export type Snap = { + permissionName: string; + id: string; + version: string; + initialPermissions: Record; +}; diff --git a/packages/nextjs/app/types/styled.d.ts b/packages/nextjs/app/types/styled.d.ts new file mode 100755 index 0000000..f8fc6ab --- /dev/null +++ b/packages/nextjs/app/types/styled.d.ts @@ -0,0 +1,19 @@ +/* eslint-disable import/no-unassigned-import */ + +import 'styled-components'; + +/** + * styled-component default theme extension + */ +declare module 'styled-components' { + /* eslint-disable @typescript-eslint/consistent-type-definitions */ + export interface DefaultTheme { + fonts: Record; + fontSizes: Record; + breakpoints: string[]; + mediaQueries: Record; + radii: Record; + shadows: Record; + colors: Record>; + } +} diff --git a/packages/nextjs/app/types/svg.d.ts b/packages/nextjs/app/types/svg.d.ts new file mode 100755 index 0000000..032d9ce --- /dev/null +++ b/packages/nextjs/app/types/svg.d.ts @@ -0,0 +1,7 @@ +/* eslint-disable import/unambiguous */ + +declare module '*.svg' { + import type { FunctionComponent, SVGProps } from 'react'; + + export const ReactComponent: FunctionComponent>; +} diff --git a/packages/nextjs/app/utils/button.ts b/packages/nextjs/app/utils/button.ts new file mode 100755 index 0000000..b951687 --- /dev/null +++ b/packages/nextjs/app/utils/button.ts @@ -0,0 +1,5 @@ +import type { Snap } from '../types'; +import { isLocalSnap } from './snap'; + +export const shouldDisplayReconnectButton = (installedSnap: Snap | null) => + installedSnap && isLocalSnap(installedSnap?.id); diff --git a/packages/nextjs/app/utils/index.ts b/packages/nextjs/app/utils/index.ts new file mode 100755 index 0000000..edce332 --- /dev/null +++ b/packages/nextjs/app/utils/index.ts @@ -0,0 +1,5 @@ +export * from './metamask'; +export * from './snap'; +export * from './theme'; +export * from './localStorage'; +export * from './button'; diff --git a/packages/nextjs/app/utils/localStorage.ts b/packages/nextjs/app/utils/localStorage.ts new file mode 100755 index 0000000..f5b6482 --- /dev/null +++ b/packages/nextjs/app/utils/localStorage.ts @@ -0,0 +1,33 @@ +/** + * Get a local storage key. + * + * @param key - The local storage key to access. + * @returns The value stored at the key provided if the key exists. + */ +export const getLocalStorage = (key: string) => { + const { localStorage: ls } = window; + + if (ls !== null) { + const data = ls.getItem(key); + return data; + } + + throw new Error('Local storage is not available.'); +}; + +/** + * Set a value to local storage at a certain key. + * + * @param key - The local storage key to set. + * @param value - The value to set. + */ +export const setLocalStorage = (key: string, value: string) => { + const { localStorage: ls } = window; + + if (ls !== null) { + ls.setItem(key, value); + return; + } + + throw new Error('Local storage is not available.'); +}; diff --git a/packages/nextjs/app/utils/metamask.ts b/packages/nextjs/app/utils/metamask.ts new file mode 100755 index 0000000..30b3971 --- /dev/null +++ b/packages/nextjs/app/utils/metamask.ts @@ -0,0 +1,115 @@ +import type { + EIP6963AnnounceProviderEvent, + MetaMaskInpageProvider, +} from '@metamask/providers'; + +/** + * Check if the current provider supports snaps by calling `wallet_getSnaps`. + * + * @param provider - The provider to use to check for snaps support. Defaults to + * `window.ethereum`. + * @returns True if the provider supports snaps, false otherwise. + */ +export async function hasSnapsSupport( + provider: MetaMaskInpageProvider = window.ethereum, +) { + try { + await provider.request({ + method: 'wallet_getSnaps', + }); + + return true; + } catch { + return false; + } +} + +/** + * Get a MetaMask provider using EIP6963. This will return the first provider + * reporting as MetaMask. If no provider is found after 500ms, this will + * return null instead. + * + * @returns A MetaMask provider if found, otherwise null. + */ +export async function getMetaMaskEIP6963Provider() { + return new Promise((rawResolve) => { + // Timeout looking for providers after 500ms + const timeout = setTimeout(() => { + resolve(null); + }, 500); + + /** + * Resolve the promise with a MetaMask provider and clean up. + * + * @param provider - A MetaMask provider if found, otherwise null. + */ + function resolve(provider: MetaMaskInpageProvider | null) { + window.removeEventListener( + 'eip6963:announceProvider', + onAnnounceProvider, + ); + clearTimeout(timeout); + rawResolve(provider); + } + + /** + * Listener for the EIP6963 announceProvider event. + * + * Resolves the promise if a MetaMask provider is found. + * + * @param event - The EIP6963 announceProvider event. + * @param event.detail - The details of the EIP6963 announceProvider event. + */ + function onAnnounceProvider({ detail }: EIP6963AnnounceProviderEvent) { + const { info, provider } = detail; + + if (info.rdns.includes('io.metamask')) { + resolve(provider); + } + } + + window.addEventListener('eip6963:announceProvider', onAnnounceProvider); + + window.dispatchEvent(new Event('eip6963:requestProvider')); + }); +} + +/** + * Get a provider that supports snaps. This will loop through all the detected + * providers and return the first one that supports snaps. + * + * @returns The provider, or `null` if no provider supports snaps. + */ +export async function getSnapsProvider() { + if (typeof window === 'undefined') { + return null; + } + + if (await hasSnapsSupport()) { + return window.ethereum; + } + + if (window.ethereum?.detected) { + for (const provider of window.ethereum.detected) { + if (await hasSnapsSupport(provider)) { + return provider; + } + } + } + + if (window.ethereum?.providers) { + for (const provider of window.ethereum.providers) { + if (await hasSnapsSupport(provider)) { + return provider; + } + } + } + + const eip6963Provider = await getMetaMaskEIP6963Provider(); + + if (eip6963Provider && (await hasSnapsSupport(eip6963Provider))) { + return eip6963Provider; + } + + return null; +} diff --git a/packages/nextjs/app/utils/snap.ts b/packages/nextjs/app/utils/snap.ts new file mode 100755 index 0000000..82fc11d --- /dev/null +++ b/packages/nextjs/app/utils/snap.ts @@ -0,0 +1,7 @@ +/** + * Check if a snap ID is a local snap ID. + * + * @param snapId - The snap ID. + * @returns True if it's a local Snap, or false otherwise. + */ +export const isLocalSnap = (snapId: string) => snapId.startsWith('local:'); diff --git a/packages/nextjs/app/utils/theme.ts b/packages/nextjs/app/utils/theme.ts new file mode 100755 index 0000000..a5b6a85 --- /dev/null +++ b/packages/nextjs/app/utils/theme.ts @@ -0,0 +1,27 @@ +import { getLocalStorage, setLocalStorage } from './localStorage'; + +/** + * Get the user's preferred theme in local storage. + * Will default to the browser's preferred theme if there is no value in local storage. + * + * @returns True if the theme is "dark" otherwise, false. + */ +export const getThemePreference = () => { + if (typeof window === 'undefined') { + return false; + } + + const darkModeSystem = window?.matchMedia( + '(prefers-color-scheme: dark)', + ).matches; + + const localStoragePreference = getLocalStorage('theme'); + const systemPreference = darkModeSystem ? 'dark' : 'light'; + const preference = localStoragePreference ?? systemPreference; + + if (!localStoragePreference) { + setLocalStorage('theme', systemPreference); + } + + return preference === 'dark'; +}; diff --git a/packages/nextjs/app/view/[address]/page.tsx b/packages/nextjs/app/view/[address]/page.tsx old mode 100644 new mode 100755 diff --git a/packages/nextjs/app/view/page.tsx b/packages/nextjs/app/view/page.tsx old mode 100644 new mode 100755