diff --git a/examples/apps/auth-sample/package.json b/examples/apps/auth-sample/package.json index b66a00f..c7259af 100644 --- a/examples/apps/auth-sample/package.json +++ b/examples/apps/auth-sample/package.json @@ -9,7 +9,7 @@ }, "dependencies": { "@heroicons/react": "^2.0.13", - "@openfort/openfort-js": "0.7.7", + "@openfort/openfort-js": "0.7.9", "@openfort/openfort-node": "^0.6.47", "@radix-ui/react-toast": "^1.1.2", "@rainbow-me/rainbowkit": "^2.1.1", diff --git a/examples/apps/auth-sample/public/walletconnect.svg b/examples/apps/auth-sample/public/walletconnect.svg new file mode 100644 index 0000000..d90457a --- /dev/null +++ b/examples/apps/auth-sample/public/walletconnect.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/apps/auth-sample/src/components/Button.tsx b/examples/apps/auth-sample/src/components/Button.tsx index 779e989..73691ea 100644 --- a/examples/apps/auth-sample/src/components/Button.tsx +++ b/examples/apps/auth-sample/src/components/Button.tsx @@ -31,7 +31,7 @@ const variantStyles = { filled: 'rounded-md bg-zinc-900 py-1 px-3 text-white hover:bg-zinc-700', outline: 'rounded-md py-1 px-3 text-zinc-700 ring-1 ring-inset ring-zinc-900/10 hover:bg-zinc-900/2.5 hover:text-zinc-900', - text: 'text-orange-600 hover:text-zinc-900', + text: 'text-blue-600 hover:text-zinc-900', }; interface ButtonProps extends ButtonHTMLAttributes { diff --git a/examples/apps/auth-sample/src/components/Layouts/AuthLayout.tsx b/examples/apps/auth-sample/src/components/Layouts/AuthLayout.tsx index 6400307..1384e41 100644 --- a/examples/apps/auth-sample/src/components/Layouts/AuthLayout.tsx +++ b/examples/apps/auth-sample/src/components/Layouts/AuthLayout.tsx @@ -12,7 +12,7 @@ export function AuthLayout({ children: React.ReactNode; }) { return ( -
+
diff --git a/examples/apps/auth-sample/src/components/NFT/MintNFTButton.tsx b/examples/apps/auth-sample/src/components/NFT/MintNFTButton.tsx index 132f505..a13d355 100644 --- a/examples/apps/auth-sample/src/components/NFT/MintNFTButton.tsx +++ b/examples/apps/auth-sample/src/components/NFT/MintNFTButton.tsx @@ -1,27 +1,47 @@ -import React, {useState} from 'react'; +import React, {useCallback, useState} from 'react'; import {useOpenfort} from '../../hooks/useOpenfort'; import {EmbeddedState} from '@openfort/openfort-js'; import Loading from '../Loading'; +import openfort from '../../utils/openfortConfig'; const MintNFTButton: React.FC<{ handleSetMessage: (message: string) => void; }> = ({handleSetMessage}) => { - const {mintNFT, state} = useOpenfort(); + const {state} = useOpenfort(); const [loading, setLoading] = useState(false); + const mintNFT = useCallback(async (): Promise => { + const collectResponse = await fetch(`/api/protected-collect`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${openfort.getAccessToken()}`, + }, + }); + + if (!collectResponse.ok) { + alert('Failed to mint NFT status: ' + collectResponse.status); + return null; + } + const collectResponseJSON = await collectResponse.json(); + + if (collectResponseJSON.data?.nextAction) { + const response = await openfort.sendSignatureTransactionIntentRequest( + collectResponseJSON.data.id, + collectResponseJSON.data.nextAction.payload.userOperationHash + ); + return response?.response?.transactionHash ?? null; + } else { + return collectResponseJSON.response?.transactionHash; + } + }, []); + const handleMintNFT = async () => { - try { - setLoading(true); - const transactionHash = await mintNFT(); - setLoading(false); - if (!transactionHash) { - throw new Error('Failed to mint NFT'); - } + setLoading(true); + const transactionHash = await mintNFT(); + setLoading(false); + if (transactionHash) { handleSetMessage(`https://www.oklink.com/amoy/tx/${transactionHash}`); - } catch (err) { - // Handle errors from minting process - console.error('Failed to mint NFT:', err); - alert('Failed to mint NFT. Please try again.'); } }; diff --git a/examples/apps/auth-sample/src/components/OAuth/LinkOAuthButton.tsx b/examples/apps/auth-sample/src/components/OAuth/LinkOAuthButton.tsx new file mode 100644 index 0000000..db11868 --- /dev/null +++ b/examples/apps/auth-sample/src/components/OAuth/LinkOAuthButton.tsx @@ -0,0 +1,51 @@ +import React, {useMemo, useState} from 'react'; +import {useOpenfort} from '../../hooks/useOpenfort'; +import {AuthPlayerResponse, OAuthProvider} from '@openfort/openfort-js'; +import Loading from '../Loading'; +import openfort from '../../utils/openfortConfig'; +import {getURL} from '../../utils/getUrl'; + +const LinkOAuthButton: React.FC<{ + provider: OAuthProvider; + user: AuthPlayerResponse | null; +}> = ({provider, user}) => { + const {state} = useOpenfort(); + const [loading, setLoading] = useState(false); + const handleLinkOAuth = async () => { + try { + setLoading(true); + const accessToken = openfort.getAccessToken() as string; + const {url} = await openfort.initLinkOAuth({ + authToken: accessToken, + provider: provider, + options: { + redirectTo: getURL() + '/login', + }, + }); + setLoading(false); + window.location.href = url; + } catch (err) { + console.error('Failed to sign message:', err); + alert('Failed to sign message. Please try again.'); + } + }; + + const isLinked = useMemo(() => { + if (!user) return false; + return user.linkedAccounts.some((account) => account.provider === provider); + }, [user]); + + return ( +
+ +
+ ); +}; + +export default LinkOAuthButton; diff --git a/examples/apps/auth-sample/src/components/Sessions/CreateSessionButton.tsx b/examples/apps/auth-sample/src/components/Sessions/CreateSessionButton.tsx new file mode 100644 index 0000000..bedd416 --- /dev/null +++ b/examples/apps/auth-sample/src/components/Sessions/CreateSessionButton.tsx @@ -0,0 +1,129 @@ +import React, {useCallback, useState} from 'react'; +import {useOpenfort} from '../../hooks/useOpenfort'; +import {EmbeddedState} from '@openfort/openfort-js'; +import Loading from '../Loading'; +import openfort from '../../utils/openfortConfig'; +import {ethers} from 'ethers'; +import MintNFTSessionButton from './MintNFTButtoSession'; + +const sessionMethods = [ + {id: '1hour', title: '1 Hour'}, + {id: '1day', title: '1 Day'}, + {id: '1month', title: '1 Month'}, +]; + +const CreateSessionButton: React.FC<{ + handleSetMessage: (message: string) => void; +}> = ({handleSetMessage}) => { + const {state, signMessage} = useOpenfort(); + const [loading, setLoading] = useState(false); + const [sessionKey, setSessionKey] = useState(null); + + const createSession = useCallback(async (): Promise<{ + address: string; + privateKey: string; + } | null> => { + const sessionKey = ethers.Wallet.createRandom(); + const sessionResponse = await fetch(`/api/protected-create-session`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${openfort.getAccessToken()}`, + }, + body: JSON.stringify({ + sessionDuration: document.querySelector( + 'input[name="session-method"]:checked' + )?.id, + sessionAddress: sessionKey.address, + }), + }); + + if (!sessionResponse.ok) { + alert('Failed to create session: ' + sessionResponse.status); + return null; + } + const sessionResponseJSON = await sessionResponse.json(); + + if (sessionResponseJSON.data?.nextAction) { + const signature = await signMessage( + sessionResponseJSON.data?.nextAction.payload.userOperationHash, + { + hashMessage: true, + arrayifyMessage: true, + } + ); + if (signature?.error) { + throw new Error(`Failed to sign message. ${signature?.error}`); + } + const response = await openfort.sendRegisterSessionRequest( + sessionResponseJSON.data.id, + signature.data as string + ); + if (!response?.isActive) { + throw new Error('Session key registration failed'); + } + setSessionKey(sessionKey.privateKey); + return {address: sessionKey.address, privateKey: sessionKey.privateKey}; + } else { + return null; + } + }, []); + + const handleCreateSession = async () => { + setLoading(true); + const session = await createSession(); + setLoading(false); + if (session) { + handleSetMessage( + `Session key registered successfully:\n Address: ${session.address}\n Private Key: ${session.privateKey}` + ); + } + }; + + return ( +
+
+
+ + Session duration + +

+ How long should the session last? +

+
+ {sessionMethods.map((sessionMethod) => ( +
+ + +
+ ))} +
+
+ +
+ +
+ ); +}; + +export default CreateSessionButton; diff --git a/examples/apps/auth-sample/src/components/Sessions/MintNFTButtoSession.tsx b/examples/apps/auth-sample/src/components/Sessions/MintNFTButtoSession.tsx new file mode 100644 index 0000000..eacfaf8 --- /dev/null +++ b/examples/apps/auth-sample/src/components/Sessions/MintNFTButtoSession.tsx @@ -0,0 +1,79 @@ +import React, {useCallback, useState} from 'react'; +import Loading from '../Loading'; +import openfort from '../../utils/openfortConfig'; +import {ethers} from 'ethers'; +import {arrayify} from 'ethers/lib/utils'; + +const MintNFTSessionButton: React.FC<{ + handleSetMessage: (message: string) => void; + sessionKey: string | null; +}> = ({handleSetMessage, sessionKey}) => { + const [loading, setLoading] = useState(false); + + const mintNFT = useCallback(async (): Promise => { + if (!sessionKey) { + return null; + } + const collectResponse = await fetch(`/api/protected-collect`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${openfort.getAccessToken()}`, + }, + }); + + if (!collectResponse.ok) { + alert('Failed to mint NFT status: ' + collectResponse.status); + return null; + } + const collectResponseJSON = await collectResponse.json(); + + if (collectResponseJSON.data?.nextAction) { + const message = arrayify( + collectResponseJSON.data.nextAction.payload.userOperationHash + ); + const sessionSigner = new ethers.Wallet(sessionKey); + const signature = await sessionSigner?.signMessage(message); + if (!signature) { + throw new Error('Failed to sign message with session key'); + } + + const response = await openfort.sendSignatureTransactionIntentRequest( + collectResponseJSON.data.id, + null, + signature + ); + return response?.response?.transactionHash ?? null; + } else { + return collectResponseJSON.response?.transactionHash; + } + }, [sessionKey]); + + const handleMintNFT = async () => { + setLoading(true); + const transactionHash = await mintNFT(); + setLoading(false); + if (transactionHash) { + handleSetMessage(`https://www.oklink.com/amoy/tx/${transactionHash}`); + } + }; + + return ( +
+ + {!sessionKey && ( +

+ Create a session before minting an NFT signed with a session key. +

+ )} +
+ ); +}; + +export default MintNFTSessionButton; diff --git a/examples/apps/auth-sample/src/components/Signatures/SignMessageButton.tsx b/examples/apps/auth-sample/src/components/Signatures/SignMessageButton.tsx index 1593d4b..59680c4 100644 --- a/examples/apps/auth-sample/src/components/Signatures/SignMessageButton.tsx +++ b/examples/apps/auth-sample/src/components/Signatures/SignMessageButton.tsx @@ -18,7 +18,6 @@ const SignMessageButton: React.FC<{ } handleSetMessage(signature.data!); } catch (err) { - // Handle errors from minting process console.error('Failed to sign message:', err); alert('Failed to sign message. Please try again.'); } diff --git a/examples/apps/auth-sample/src/components/WalletConnectButton.tsx b/examples/apps/auth-sample/src/components/WalletConnectButton.tsx index 0a6d191..c08b38c 100644 --- a/examples/apps/auth-sample/src/components/WalletConnectButton.tsx +++ b/examples/apps/auth-sample/src/components/WalletConnectButton.tsx @@ -1,10 +1,10 @@ -import {sepolia} from 'wagmi/chains'; +import {polygonAmoy} from 'wagmi/chains'; import React, {FunctionComponent, useState} from 'react'; import {withWagmi} from './wagmiProvider'; -import {Connector, createConfig, http, useConnect} from 'wagmi'; +import {Connector, createConfig, http, useAccount, useConnect} from 'wagmi'; import {Chain, WalletConnector} from '../utils/constants'; import type {Chain as WagmiChain} from 'wagmi/chains'; -import {metaMask, walletConnect, coinbaseWallet} from 'wagmi/connectors'; +import {metaMask, walletConnect} from 'wagmi/connectors'; import {Transport} from '@wagmi/core'; import {OnSuccess, SignMessageFunction} from '../utils/types'; import {createSIWEMessage} from '../utils/create-siwe-message'; @@ -18,6 +18,7 @@ type GetWalletButtonsParams = { type WalletConnectButtonsOwnProps = { onSuccess: OnSuccess; + link: boolean; }; type WalletConnectButtonsProps = WalletConnectButtonsOwnProps & { @@ -61,10 +62,13 @@ const WalletConnectButton = ({ const WalletConnectButtons = ({ onSuccess, + link, signMessage, }: WalletConnectButtonsProps) => { const {connectors, connect} = useConnect(); + const [loading, setLoading] = useState(null!); + const initConnect = async (connector: Connector) => { setLoading(connector.id); connect( @@ -78,17 +82,26 @@ const WalletConnectButtons = ({ }, onSuccess: async ({accounts}) => { const address = accounts[0]; + if (connector.name === 'Openfort') onSuccess(); if (address) { try { const {nonce} = await openfort.initSIWE({address}); const SIWEMessage = createSIWEMessage(address, nonce); const signature = await signMessage(SIWEMessage); - await openfort.authenticateWithSIWE({ - signature, - message: SIWEMessage, - connectorType: 'metamask', - walletClientType: 'injected', - }); + link + ? await openfort.linkWallet({ + authToken: openfort.getAccessToken() as string, + signature, + message: SIWEMessage, + connectorType: connector?.type, + walletClientType: connector?.name, + }) + : await openfort.authenticateWithSIWE({ + signature, + message: SIWEMessage, + connectorType: connector?.type, + walletClientType: connector?.name, + }); onSuccess(); } finally { setLoading(null!); @@ -109,9 +122,7 @@ const WalletConnectButtons = ({ icon={`${connector.type.toLowerCase()}.${ connector.type === 'injected' ? 'webp' : 'svg' }`} - title={ - connector.type === 'injected' ? 'Browser wallet' : connector.name - } + title={connector.name} /> ))} @@ -119,12 +130,11 @@ const WalletConnectButtons = ({ }; const chainToWagmiChain = { - [Chain.SEPOLIA]: sepolia, + [Chain.AMOY]: polygonAmoy, }; const connectorToWagmiConnector = { [WalletConnector.METAMASK]: metaMask, [WalletConnector.WALLET_CONNECT]: walletConnect, - [WalletConnector.COINBASE]: coinbaseWallet, }; export const getWalletButtons = (params: GetWalletButtonsParams) => { @@ -141,9 +151,7 @@ export const getWalletButtons = (params: GetWalletButtonsParams) => { ? connectorToWagmiConnector[connector]({ projectId: params.walletConnectProjectId as string, }) - : connector === 'metaMask' - ? connectorToWagmiConnector[connector]({dappMetadata: {name: 'Openfort'}}) - : connectorToWagmiConnector[connector]() + : connectorToWagmiConnector[connector]({dappMetadata: {name: 'Openfort'}}) ); const config = createConfig({ chains: chains as any, diff --git a/examples/apps/auth-sample/src/hooks/useOpenfort.tsx b/examples/apps/auth-sample/src/hooks/useOpenfort.tsx index ba9cb04..c785e3f 100644 --- a/examples/apps/auth-sample/src/hooks/useOpenfort.tsx +++ b/examples/apps/auth-sample/src/hooks/useOpenfort.tsx @@ -31,9 +31,9 @@ type ContextType = { ) => Promise; auth: (accessToken: string) => Promise; signMessage: ( - hashedMessage: string + hashedMessage: string, + options?: {hashMessage: boolean; arrayifyMessage: boolean} ) => Promise<{data?: string; error?: Error}>; - mintNFT: () => Promise; signTypedData: ( domain: TypedDataDomain, types: Record>, @@ -106,36 +106,13 @@ const OpenfortProvider = ({children}: PropsWithChildren) => { [] ); - const mintNFT = useCallback(async (): Promise => { - const collectResponse = await fetch(`/api/protected-collect`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${openfort.getAccessToken()}`, - }, - }); - - if (!collectResponse.ok) { - alert('Failed to mint NFT status: ' + collectResponse.status); - throw new Error('Failed to perform API call'); - } - const collectResponseJSON = await collectResponse.json(); - - if (collectResponseJSON.data?.nextAction) { - const response = await openfort.sendSignatureTransactionIntentRequest( - collectResponseJSON.data.id, - collectResponseJSON.data.nextAction.payload.userOperationHash - ); - return response.response?.transactionHash; - } else { - return collectResponseJSON.response?.transactionHash; - } - }, []); - const signMessage = useCallback( - async (message: string): Promise<{data?: string; error?: Error}> => { + async ( + message: string, + options?: {hashMessage: boolean; arrayifyMessage: boolean} + ): Promise<{data?: string; error?: Error}> => { try { - const data = await openfort.signMessage(message); + const data = await openfort.signMessage(message, options); return {data: data}; } catch (err) { console.log('Error signing message:', err); @@ -217,7 +194,6 @@ const OpenfortProvider = ({children}: PropsWithChildren) => { signMessage, signTypedData, logout, - mintNFT, }} > {children} diff --git a/examples/apps/auth-sample/src/pages/api/protected-create-session.ts b/examples/apps/auth-sample/src/pages/api/protected-create-session.ts new file mode 100644 index 0000000..95d2858 --- /dev/null +++ b/examples/apps/auth-sample/src/pages/api/protected-create-session.ts @@ -0,0 +1,72 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import openfort from '../../utils/openfortAdminConfig'; + +const policy_id = 'pol_e7491b89-528e-40bb-b3c2-9d40afa4fefc'; +const chainId = 80002; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + const accessToken = req.headers.authorization?.split(' ')[1]; + if (!accessToken) { + return res.status(401).send({ + error: 'You must be signed in to view the protected content on this page.', + }); + } + + try { + const response = await openfort.iam.verifyAuthToken( + accessToken, + ); + + if (!response?.playerId) { + return res.status(401).send({ + error: 'Invalid token or unable to verify user.', + }); + } + + const { sessionDuration, sessionAddress } = req.body; + if (!sessionDuration || !sessionAddress) { + return res.status(400).send({ + error: 'Session duration and sessionAddress are required', + }); + } + const sessionDurationNumber: { [key: string]: number } = { + '1hour': 3600000, + '1day': 86400000, + '1month': 2592000000, + } + if (!sessionDurationNumber[sessionDuration]) { + return res.status(400).send({ + error: 'Invalid session duration', + }); + } + + + // The unix timestamp in seconds when the session key becomes valid in number format. + const validAfter = Math.floor(new Date().getTime() / 1000) + // The unix timestamp in seconds when the session key becomes invalid in number format (where session duration is 1hour, 1day, 1month). + const validUntil = Math.floor(new Date(Date.now() + sessionDurationNumber[sessionDuration]).getTime() / 1000) + + const playerId = response.playerId; + + const sessionRegistration = await openfort.sessions.create({ + player: playerId, + policy: policy_id, + chainId, + address: sessionAddress, + validAfter: Number(validAfter), + validUntil: Number(validUntil), + }); + + res.send({ + data: sessionRegistration, + }); + } catch (e) { + console.error(e); + res.status(500).send({ + error: 'Internal server error', + }); + } +} diff --git a/examples/apps/auth-sample/src/pages/connect-wallet.tsx b/examples/apps/auth-sample/src/pages/connect-wallet.tsx index e636ec5..843964e 100644 --- a/examples/apps/auth-sample/src/pages/connect-wallet.tsx +++ b/examples/apps/auth-sample/src/pages/connect-wallet.tsx @@ -10,7 +10,7 @@ import {useRouter} from 'next/router'; import {getWalletButtons} from '../components/WalletConnectButton'; import {Chain, WalletConnector} from '../utils/constants'; -function RegisterPage() { +function ConnectWalletPage() { const [user, setUser] = useState(null); const router = useRouter(); @@ -26,8 +26,8 @@ function RegisterPage() { const [status, setStatus] = useState(null); const WalletButtons = getWalletButtons({ - chains: [Chain.SEPOLIA], - connectors: [WalletConnector.METAMASK, WalletConnector.COINBASE], + chains: [Chain.AMOY], + connectors: [WalletConnector.METAMASK], }); const redirect = () => { @@ -48,14 +48,14 @@ function RegisterPage() { subtitle={ <> {'Have an account? '} - + Sign in } >
- +
@@ -63,4 +63,4 @@ function RegisterPage() { ); } -export default RegisterPage; +export default ConnectWalletPage; diff --git a/examples/apps/auth-sample/src/pages/forgot-password.tsx b/examples/apps/auth-sample/src/pages/forgot-password.tsx index b848a3d..10c4350 100644 --- a/examples/apps/auth-sample/src/pages/forgot-password.tsx +++ b/examples/apps/auth-sample/src/pages/forgot-password.tsx @@ -82,7 +82,7 @@ function ForgotPasswordPage() { subtitle={ <> Already have an account?{' '} - + Sign in diff --git a/examples/apps/auth-sample/src/pages/index.tsx b/examples/apps/auth-sample/src/pages/index.tsx index 3f704d2..00d8aaf 100644 --- a/examples/apps/auth-sample/src/pages/index.tsx +++ b/examples/apps/auth-sample/src/pages/index.tsx @@ -2,7 +2,7 @@ import React, {useEffect, useMemo, useRef, useState} from 'react'; import {NextPage} from 'next'; import MintNFTButton from '../components/NFT/MintNFTButton'; import {useOpenfort} from '../hooks/useOpenfort'; -import {EmbeddedState} from '@openfort/openfort-js'; +import {EmbeddedState, OAuthProvider} from '@openfort/openfort-js'; import AccountRecovery from '../components/EmbeddedSignerRecovery/AccountRecovery'; import SignMessageButton from '../components/Signatures/SignMessageButton'; import SignTypedDataButton from '../components/Signatures/SignTypedDataButton'; @@ -16,6 +16,8 @@ import LogoutButton from '../components/Logout'; import Link from 'next/link'; import {Logo} from '../components/Logo'; import GetUserButton from '../components/User/GetUserButton'; +import CreateSessionButton from '../components/Sessions/CreateSessionButton'; +import LinkOAuthButton from '../components/OAuth/LinkOAuthButton'; const HomePage: NextPage = () => { const {state} = useOpenfort(); @@ -34,12 +36,13 @@ const HomePage: NextPage = () => { const sessionData = await openfort.getUser().catch((error: Error) => { console.log('error', error); }); - if (sessionData) setUser(sessionData); - else router.push('/login'); + if (sessionData) { + setUser(sessionData); + handleSetMessage(JSON.stringify(sessionData, null, 2)); + } else router.push('/login'); }; - if (state === EmbeddedState.UNAUTHENTICATED) router.push('/login'); - fetchUser(); - }, [openfort, state]); + if (!user) fetchUser(); + }, [openfort]); useEffect(() => { if (textareaRef.current) { @@ -57,7 +60,7 @@ const HomePage: NextPage = () => { useEffect(() => { if (linkedAccount?.verified === false) { - router.push('/login'); + router.push('/register'); } }, [linkedAccount, router]); @@ -65,7 +68,9 @@ const HomePage: NextPage = () => { return (
}>
-

Welcome, {user?.id}!

+

+ Welcome, {user?.player?.name ?? user?.id}! +

{' '} @@ -87,7 +92,7 @@ const HomePage: NextPage = () => { } return ( -
+
@@ -103,14 +108,14 @@ const HomePage: NextPage = () => {