diff --git a/components/login-iron-session.tsx b/components/login-iron-session.tsx new file mode 100644 index 0000000000000000000000000000000000000000..0539ba6bb853427854f557a4f0c37f2b5f44c27c --- /dev/null +++ b/components/login-iron-session.tsx @@ -0,0 +1,142 @@ +import { useState } from 'react'; + +import { useSession, signIn, signOut, getCsrfToken } from 'next-auth/react'; +import AccountSelect from './account-select'; + +import { useRouter } from 'next/router'; + +import styles from '@/styles/Home.module.css'; +import { Inter } from 'next/font/google'; +import Link from 'next/link'; +import { usePolkadotExtensionWithContext } from '@/context/polkadotExtensionContext'; +const inter = Inter({ subsets: ['latin'] }); + +export default function LoginButton() { + const router = useRouter(); + const [error, setError] = useState<string | undefined>(undefined); + const [isLoading, setIsLoading] = useState(false); + const [session, setSession] = useState(null); + + const { accounts, actingAccount, injector } = usePolkadotExtensionWithContext(); + // we can use web3FromSource which will return an InjectedExtension type + + const signIn = async (credentials: any) => { + const options: RequestInit = { + method: 'POST', + headers: { + accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(credentials), + }; + + const response = fetch('/api/auth/login-with-iron-session', options); + const jsonData = (await response).json(); + + return jsonData; + }; + + const handleLogin = async () => { + try { + setIsLoading(true); + let signature = ''; + const message = { + address: actingAccount?.address, + statement: 'Sign in with polkadot extension to the example tokengated example dApp', + uri: window.location.origin, + version: '1', + nonce: await getCsrfToken(), + }; + + const signRaw = injector?.signer?.signRaw; + + if (!!signRaw && !!actingAccount) { + // after making sure that signRaw is defined + // we can use it to sign our message + const data = await signRaw({ + address: actingAccount.address, + data: JSON.stringify(message), + type: 'bytes', + }); + + signature = data.signature; + } + + const result = await signIn({ + redirect: false, + callbackUrl: '/protected-api', + message: JSON.stringify(message), + name: actingAccount?.meta?.name, + signature, + address: actingAccount?.address, + }); + + // take the user to the protected page if they are allowed + if (result?.user) { + router.push('/protected-api'); + } + + setError(result?.error); + setIsLoading(false); + } catch (error) { + setError('Cancelled Signature'); + setIsLoading(false); + } + }; + + return ( + <> + {accounts && accounts.length > 0 ? ( + <> + <div className={styles.cardWrap}> + <div className={styles.dropDownWrap}>{!session && <AccountSelect />}</div> + {session ? ( + <> + <Link href="/protected-api" className={styles.card}> + <h2 className={inter.className}> + 🎉 View Tokengated Route <span>-></span> + </h2> + <p className={inter.className}> + You passed the tokengate {session.user?.name}. You can now view the protected + route. + </p> + </Link> + <div role="button" onClick={() => signOut()} className={styles.card}> + <h2 className={inter.className}> + Sign Out <span>-></span> + </h2> + <p className={inter.className}> + Click here to sign out your account {session.user?.name}. + </p> + </div> + </> + ) : ( + <div role="button" onClick={() => handleLogin()} className={styles.card}> + <h2 className={inter.className}> + 🔑 Let me in <span>-></span> + </h2> + <p className={inter.className}> + Click here to sign in with your selected account and check if you can view the + tokengated content. <br></br> + You need > 1 KSM free balance. + </p> + </div> + )} + </div> + {isLoading ? <>Signing In ...</> : <span className={styles.error}> {error} </span>} + </> + ) : ( + <div className={styles.walletInfo}> + <p> + Please{' '} + <a className={styles.colorA} href="https://polkadot.js.org/extension/"> + install a polkadot wallet browser extension + </a>{' '} + to test this dApp. + </p> + <p>If you have already installed it allow this application to access it.</p> + </div> + )} + </> + ); +} diff --git a/components/login.tsx b/components/login.tsx index 66acd2db7abbdc509badd47dae4fae7eaec3e35b..3625e83b1ad4c4eda73a095e6bab433ad550da44 100644 --- a/components/login.tsx +++ b/components/login.tsx @@ -1,49 +1,47 @@ -import { useState } from "react"; +import { useState } from 'react'; -import { useSession, signIn, signOut, getCsrfToken } from "next-auth/react" -import AccountSelect from "./account-select"; +import { useSession, signIn, signOut, getCsrfToken } from 'next-auth/react'; +import AccountSelect from './account-select'; -import { useRouter } from "next/router"; +import { useRouter } from 'next/router'; -import styles from '@/styles/Home.module.css' -import { Inter } from "next/font/google"; -import Link from "next/link"; -import { usePolkadotExtensionWithContext } from "@/context/polkadotExtensionContext"; -const inter = Inter({ subsets: ['latin'] }) +import styles from '@/styles/Home.module.css'; +import { Inter } from 'next/font/google'; +import Link from 'next/link'; +import { usePolkadotExtensionWithContext } from '@/context/polkadotExtensionContext'; +const inter = Inter({ subsets: ['latin'] }); export default function LoginButton() { - const router = useRouter() - const [error, setError] = useState<string | undefined>(undefined) - const [isLoading, setIsLoading] = useState( false ) + const router = useRouter(); + const [error, setError] = useState<string | undefined>(undefined); + const [isLoading, setIsLoading] = useState(false); - const { accounts, actingAccount, injector } = usePolkadotExtensionWithContext() + const { accounts, actingAccount, injector } = usePolkadotExtensionWithContext(); // we can use web3FromSource which will return an InjectedExtension type const handleLogin = async () => { try { - setIsLoading( true ) - let signature = '' + setIsLoading(true); + let signature = ''; const message = { - domain: window.location.host, - address: actingAccount?.address, statement: 'Sign in with polkadot extension to the example tokengated example dApp', uri: window.location.origin, version: '1', nonce: await getCsrfToken(), - } + }; const signRaw = injector?.signer?.signRaw; if (!!signRaw && !!actingAccount) { - // after making sure that signRaw is defined - // we can use it to sign our message - const data = await signRaw({ - address: actingAccount.address, - data: JSON.stringify(message), - type: "bytes" - }); + // after making sure that signRaw is defined + // we can use it to sign our message + const data = await signRaw({ + address: actingAccount.address, + data: JSON.stringify(message), + type: 'bytes', + }); - signature = data.signature + signature = data.signature; } // will return a promise https://next-auth.js.org/getting-started/client#using-the-redirect-false-option @@ -53,86 +51,77 @@ export default function LoginButton() { message: JSON.stringify(message), name: actingAccount?.meta?.name, signature, - address: actingAccount?.address - }) + address: actingAccount?.address, + }); // take the user to the protected page if they are allowed - if(result?.url) { - router.push("/protected-api"); + if (result?.url) { + router.push('/protected-api'); } - setError( result?.error ) - setIsLoading( false ) - + setError(result?.error); + setIsLoading(false); } catch (error) { - setError( 'Cancelled Signature' ) - setIsLoading( false ) + setError('Cancelled Signature'); + setIsLoading(false); } - } + }; + + const { data: session } = useSession(); - const { data: session } = useSession() - return ( <> - { accounts && accounts.length > 0 ? - <> - <div className={ styles.cardWrap }> - <div className={ styles.dropDownWrap }> - { ! session && - <AccountSelect/> - } - </div> - { session ? - <> - <Link - href="/protected-api" - className={styles.card} - > - <h2 className={inter.className}> - 🎉 View Tokengated Route <span>-></span> - </h2> - <p className={inter.className}> - You passed the tokengate { session.user?.name }. You can now view the protected route. - </p> - </Link> - <div - role="button" - onClick={() => signOut()} - className={styles.card} - > - <h2 className={inter.className}> - Sign Out <span>-></span> - </h2> - <p className={inter.className}> - Click here to sign out your account { session.user?.name }. - </p> - </div> - </> - : - <div - role="button" - onClick={() => handleLogin()} - className={styles.card} - > + {accounts && accounts.length > 0 ? ( + <> + <div className={styles.cardWrap}> + <div className={styles.dropDownWrap}>{!session && <AccountSelect />}</div> + {session ? ( + <> + <Link href="/protected-api" className={styles.card}> + <h2 className={inter.className}> + 🎉 View Tokengated Route <span>-></span> + </h2> + <p className={inter.className}> + You passed the tokengate {session.user?.name}. You can now view the protected + route. + </p> + </Link> + <div role="button" onClick={() => signOut()} className={styles.card}> + <h2 className={inter.className}> + Sign Out <span>-></span> + </h2> + <p className={inter.className}> + Click here to sign out your account {session.user?.name}. + </p> + </div> + </> + ) : ( + <div role="button" onClick={() => handleLogin()} className={styles.card}> <h2 className={inter.className}> 🔑 Let me in <span>-></span> </h2> <p className={inter.className}> - Click here to sign in with your selected account and check if - you can view the tokengated content. <br></br> + Click here to sign in with your selected account and check if you can view the + tokengated content. <br></br> You need > 1 KSM free balance. </p> </div> - } + )} </div> - { isLoading ? <>Signing In ...</> : <span className={ styles.error }> { error } </span> } + {isLoading ? <>Signing In ...</> : <span className={styles.error}> {error} </span>} </> - : - <div className={ styles.walletInfo }> - <p>Please <a className={ styles.colorA } href="https://polkadot.js.org/extension/">install a polkadot wallet browser extension</a> to test this dApp.</p> + ) : ( + <div className={styles.walletInfo}> + <p> + Please{' '} + <a className={styles.colorA} href="https://polkadot.js.org/extension/"> + install a polkadot wallet browser extension + </a>{' '} + to test this dApp. + </p> <p>If you have already installed it allow this application to access it.</p> </div> - } + )} </> - ) -} \ No newline at end of file + ); +} diff --git a/context/polkadotExtensionContext.tsx b/context/polkadotExtensionContext.tsx index 1e3e36b4e61adb5c373d85e878964e781b9fb349..e3c9921f082b451c42c6359b4e2dbe177a215fba 100644 --- a/context/polkadotExtensionContext.tsx +++ b/context/polkadotExtensionContext.tsx @@ -2,16 +2,11 @@ // this component is used in _app.tsx // // Path: context/polkadot-extension-context.tsx -import { - createContext, - ReactNode, - useContext, - -} from "react"; +import { createContext, ReactNode, useContext } from 'react'; import { usePolkadotExtension, UsePolkadotExtensionReturnType, -} from "@/hooks/use-polkadot-extension"; +} from '@/hooks/use-polkadot-extension'; const PolkadotExtensionContext = createContext<UsePolkadotExtensionReturnType>({ accounts: [], @@ -22,14 +17,9 @@ const PolkadotExtensionContext = createContext<UsePolkadotExtensionReturnType>({ setActingAccountIdx: () => {}, }); -export const usePolkadotExtensionWithContext = () => - useContext(PolkadotExtensionContext); +export const usePolkadotExtensionWithContext = () => useContext(PolkadotExtensionContext); -export const PolkadotExtensionContextProvider = ({ - children, -}: { - children: ReactNode; -}) => { +export const PolkadotExtensionContextProvider = ({ children }: { children: ReactNode }) => { const polkadotExtension = usePolkadotExtension(); return ( diff --git a/hooks/use-polkadot-extension.ts b/hooks/use-polkadot-extension.ts index 036053204b0cebb623a72b6f86c725e649f52a48..ef6c1c0fd38435f3a945996ea590f6cb4e0f721f 100644 --- a/hooks/use-polkadot-extension.ts +++ b/hooks/use-polkadot-extension.ts @@ -1,9 +1,6 @@ -import { - InjectedAccountWithMeta, - InjectedExtension, -} from "@polkadot/extension-inject/types"; -import { useEffect, useState } from "react"; -import { documentReadyPromise } from "./utils"; +import { InjectedAccountWithMeta, InjectedExtension } from '@polkadot/extension-inject/types'; +import { useEffect, useState } from 'react'; +import { documentReadyPromise } from './utils'; export interface UsePolkadotExtensionReturnType { isReady: boolean; @@ -16,12 +13,8 @@ export interface UsePolkadotExtensionReturnType { export const usePolkadotExtension = (): UsePolkadotExtensionReturnType => { const [isReady, setIsReady] = useState(false); - const [accounts, setAccounts] = useState<InjectedAccountWithMeta[] | null>( - null - ); - const [extensions, setExtensions] = useState<InjectedExtension[] | null>( - null - ); + const [accounts, setAccounts] = useState<InjectedAccountWithMeta[] | null>(null); + const [extensions, setExtensions] = useState<InjectedExtension[] | null>(null); const [actingAccountIdx, setActingAccountIdx] = useState<number>(0); const [error, setError] = useState<Error | null>(null); const [injector, setInjector] = useState<InjectedExtension | null>(null); @@ -31,18 +24,18 @@ export const usePolkadotExtension = (): UsePolkadotExtensionReturnType => { useEffect(() => { // This effect is used to setup the browser extension const extensionSetup = async () => { - const extensionDapp = await import("@polkadot/extension-dapp"); + const extensionDapp = await import('@polkadot/extension-dapp'); const { web3AccountsSubscribe, web3Enable } = extensionDapp; const injectedPromise = documentReadyPromise(() => - web3Enable("Polkadot Tokengated Website Demo") + web3Enable('Polkadot Tokengated Website Demo'), ); const extensions = await injectedPromise; setExtensions(extensions); if (extensions.length === 0) { - console.log("no extension"); + console.log('no extension'); return; } @@ -70,11 +63,9 @@ export const usePolkadotExtension = (): UsePolkadotExtensionReturnType => { // This effect is used to get the injector from the selected account // and is triggered when the accounts or the actingAccountIdx change const getInjector = async () => { - const { web3FromSource } = await import("@polkadot/extension-dapp"); + const { web3FromSource } = await import('@polkadot/extension-dapp'); const actingAccount = - accounts && actingAccountIdx !== undefined - ? accounts[actingAccountIdx] - : undefined; + accounts && actingAccountIdx !== undefined ? accounts[actingAccountIdx] : undefined; if (actingAccount?.meta.source) { try { const injector = await web3FromSource(actingAccount?.meta.source); diff --git a/pages/_app.tsx b/pages/_app.tsx index 5974864f701b45fbe76fcec84a2e6a033a603476..533b1e40005aff159ae9c8b726a939fb5de481e3 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -1,16 +1,10 @@ -import '@/styles/globals.css' -import type { AppProps } from 'next/app' -import { SessionProvider } from "next-auth/react" -import { PolkadotExtensionContextProvider } from '@/context/polkadotExtensionContext' -import { Analytics } from '@vercel/analytics/react' - -export default function App( - { - Component, - pageProps: { session, ...pageProps } - }: AppProps -) { +import '@/styles/globals.css'; +import type { AppProps } from 'next/app'; +import { SessionProvider } from 'next-auth/react'; +import { PolkadotExtensionContextProvider } from '@/context/polkadotExtensionContext'; +import { Analytics } from '@vercel/analytics/react'; +export default function App({ Component, pageProps: { session, ...pageProps } }: AppProps) { return ( <SessionProvider session={session}> <PolkadotExtensionContextProvider> @@ -18,5 +12,5 @@ export default function App( <Analytics /> </PolkadotExtensionContextProvider> </SessionProvider> - ) + ); } diff --git a/pages/api/auth/[...nextauth].ts b/pages/api/auth/[...nextauth].ts index 664395fb2276018d06f03516d5da5242c1d1c43c..09eff2f36c9be19286be46c297e60f57b404c038 100644 --- a/pages/api/auth/[...nextauth].ts +++ b/pages/api/auth/[...nextauth].ts @@ -64,7 +64,7 @@ export const authOptions: NextAuthOptions = { try { const message = JSON.parse(credentials.message); - //verify the message is from the same domain + //verify the message is from the same uri if (message.uri !== process.env.NEXTAUTH_URL) { return Promise.reject(new Error('🚫 You shall not pass!')); }