From 9cf9f737f04d6452345edfc6c94908465573586f Mon Sep 17 00:00:00 2001 From: gagdiez Date: Fri, 9 Aug 2024 19:30:31 +0200 Subject: [PATCH 01/22] wip: reducing dependency on the VM --- src/components/NTFImage.tsx | 13 +- src/components/WalletSelector.ts | 199 +++++++++++ .../marketing-navigation/UserDropdownMenu.tsx | 53 +-- src/components/sidebar-navigation/Sidebar.tsx | 6 +- src/components/vm/VM.tsx | 99 ++++++ src/components/vm/VmCommitButton.tsx | 20 -- src/components/vm/VmComponent.tsx | 19 +- src/components/vm/VmInitializer.tsx | 327 ------------------ src/components/wallet-utilities/SendNear.tsx | 21 +- src/hooks/useSignInRedirect.ts | 14 +- src/pages/_app.tsx | 42 ++- src/pages/settings.tsx | 92 ----- src/pages/wallet-utilities.tsx | 6 +- 13 files changed, 402 insertions(+), 509 deletions(-) create mode 100644 src/components/WalletSelector.ts create mode 100644 src/components/vm/VM.tsx delete mode 100644 src/components/vm/VmCommitButton.tsx delete mode 100644 src/components/vm/VmInitializer.tsx delete mode 100644 src/pages/settings.tsx diff --git a/src/components/NTFImage.tsx b/src/components/NTFImage.tsx index bd4396114..2119372f7 100644 --- a/src/components/NTFImage.tsx +++ b/src/components/NTFImage.tsx @@ -2,7 +2,8 @@ import Image from 'next/image'; import { useCallback, useEffect, useState } from 'react'; import styled from 'styled-components'; -import { useVmStore } from '@/stores/vm'; +import { useContext } from 'react'; +import { NearContext } from './WalletSelector'; const RoundedImage = styled(Image)` border-radius: 50%; @@ -22,15 +23,15 @@ interface NftImageProps { const DEFAULT_IMAGE = 'https://ipfs.near.social/ipfs/bafkreidoxgv2w7kmzurdnmflegkthgzaclgwpiccgztpkfdkfzb4265zuu'; export const NftImage: React.FC = ({ nft, ipfs_cid, alt }) => { - const near = useVmStore((store) => store.near); + const { wallet } = useContext(NearContext); const [imageUrl, setImageUrl] = useState(DEFAULT_IMAGE); const fetchNftData = useCallback(async () => { - if (!near || !nft || !nft.contractId || !nft.tokenId || ipfs_cid) return; + if (!wallet || !nft || !nft.contractId || !nft.tokenId || ipfs_cid) return; const [nftMetadata, tokenData] = await Promise.all([ - near.viewCall(nft.contractId, 'nft_metadata'), - near.viewCall(nft.contractId, 'nft_token', { token_id: nft.tokenId }), + wallet.viewMethod({ contractId: nft.contractId, method: 'nft_metadata' }), + wallet.viewMethod({ contractId: nft.contractId, method: 'nft_token', args: { token_id: nft.tokenId } }), ]); const tokenMetadata = tokenData.metadata; @@ -43,7 +44,7 @@ export const NftImage: React.FC = ({ nft, ipfs_cid, alt }) => { } else if (tokenMedia.startsWith('Qm') || tokenMedia.startsWith('ba')) { setImageUrl(`https://ipfs.near.social/ipfs/${tokenMedia}`); } - }, [near, nft, ipfs_cid]); + }, [wallet, nft, ipfs_cid]); useEffect(() => { if (ipfs_cid) { diff --git a/src/components/WalletSelector.ts b/src/components/WalletSelector.ts new file mode 100644 index 000000000..b1e7fda6e --- /dev/null +++ b/src/components/WalletSelector.ts @@ -0,0 +1,199 @@ +// near api js +import { providers, utils } from 'near-api-js'; + +// wallet selector +import { distinctUntilChanged, map } from 'rxjs'; + +import { Context, createContext } from 'react'; + +import { setupKeypom } from '@keypom/selector'; +import type { WalletSelector, WalletSelectorState } from '@near-wallet-selector/core'; +import { setupWalletSelector } from '@near-wallet-selector/core'; +import { setupHereWallet } from '@near-wallet-selector/here-wallet'; +import { setupLedger } from '@near-wallet-selector/ledger'; +import { setupMeteorWallet } from '@near-wallet-selector/meteor-wallet'; +import { setupMintbaseWallet } from '@near-wallet-selector/mintbase-wallet'; +import { setupModal } from '@near-wallet-selector/modal-ui'; +import { setupMyNearWallet } from '@near-wallet-selector/my-near-wallet'; +import { setupNearMobileWallet } from '@near-wallet-selector/near-mobile-wallet'; +import { setupNeth } from '@near-wallet-selector/neth'; +import { setupNightly } from '@near-wallet-selector/nightly'; +import { setupSender } from '@near-wallet-selector/sender'; +import { setupWelldoneWallet } from '@near-wallet-selector/welldone-wallet'; +import { setupFastAuthWallet } from 'near-fastauth-wallet'; + +import { signInContractId, networkId as defaultNetwork } from '@/utils/config'; +import { KEYPOM_OPTIONS } from '@/utils/keypom-options'; +import type { NetworkId } from '@/utils/types'; + +const THIRTY_TGAS = '30000000000000'; +const NO_DEPOSIT = '0'; + +export class Wallet { + private createAccessKeyFor: string; + private networkId: NetworkId; + selector: Promise; + + constructor({ + networkId = defaultNetwork, + createAccessKeyFor = signInContractId, + }: { + networkId: NetworkId; + createAccessKeyFor: string; + }) { + this.createAccessKeyFor = createAccessKeyFor; + this.networkId = networkId; + } + + startUp = async (accountChangeHook: (account: string) => void) => { + this.selector = setupWalletSelector({ + network: this.networkId, + modules: [ + setupMyNearWallet(), + setupSender(), + setupHereWallet(), + setupMintbaseWallet(), + setupMeteorWallet(), + setupNeth({ + gas: '300000000000000', + bundle: false, + }), + setupNightly(), + setupWelldoneWallet(), + setupFastAuthWallet({ + walletUrl: + this.networkId === 'testnet' + ? 'https://wallet.testnet.near.org/fastauth' + : 'https://wallet.near.org/fastauth', + relayerUrl: + this.networkId === 'testnet' + ? 'http://34.70.226.83:3030/relay' + : 'https://near-relayer-mainnet.api.pagoda.co/relay', + }), + setupKeypom({ + trialAccountSpecs: { + url: + this.networkId === 'testnet' + ? 'https://test.near.org/#trial-url/ACCOUNT_ID/SECRET_KEY' + : 'https://dev.near.org/#trial-url/ACCOUNT_ID/SECRET_KEY', + modalOptions: KEYPOM_OPTIONS(this.networkId), + }, + instantSignInSpecs: { + url: + this.networkId == 'testnet' + ? 'https://test.near.org/#instant-url/ACCOUNT_ID/SECRET_KEY/MODULE_ID' + : 'https://dev.near.org/#instant-url/ACCOUNT_ID/SECRET_KEY/MODULE_ID', + }, + networkId: this.networkId, + signInContractId, + }) as any, // TODO: Refactor setupKeypom() to TS + setupLedger(), + setupNearMobileWallet(), + ], + }); + + const walletSelector = await this.selector; + const isSignedIn = walletSelector.isSignedIn(); + const accountId = isSignedIn ? walletSelector.store.getState().accounts[0].accountId : ''; + + walletSelector.store.observable + .pipe( + map((state: WalletSelectorState) => state.accounts), + distinctUntilChanged(), + ) + .subscribe((accounts: any) => { + const signedAccount = accounts.find((account: { active: boolean }) => account.active)?.accountId; + accountChangeHook(signedAccount); + }); + + return accountId; + }; + + signIn = async () => { + const modal = setupModal(await this.selector, { contractId: this.createAccessKeyFor }); + modal.show(); + }; + + signOut = async () => { + const selectedWallet = await (await this.selector).wallet(); + selectedWallet.signOut(); + }; + + viewMethod = async ({ contractId, method, args = {} }: { contractId: string; method: string; args: object }) => { + const url = `https://rpc.${this.networkId}.near.org`; + const provider = new providers.JsonRpcProvider({ url }); + + const res = await provider.query({ + request_type: 'call_function', + account_id: contractId, + method_name: method, + args_base64: Buffer.from(JSON.stringify(args)).toString('base64'), + finality: 'optimistic', + }); + return JSON.parse(Buffer.from(res.result).toString()); + }; + + callMethod = async ({ + contractId, + method, + args = {}, + gas = THIRTY_TGAS, + deposit = NO_DEPOSIT, + }: { + contractId: string; + method: string; + args: object; + gas: string; + deposit: string; + }) => { + // Sign a transaction with the "FunctionCall" action + const selectedWallet = await (await this.selector).wallet(); + const outcome = await selectedWallet.signAndSendTransaction({ + receiverId: contractId, + actions: [ + { + type: 'FunctionCall', + params: { + methodName: method, + args, + gas, + deposit, + }, + }, + ], + }); + + return providers.getTransactionLastResult(outcome); + }; + + getTransactionResult = async (txhash: string) => { + const walletSelector = await this.selector; + const { network } = walletSelector.options; + const provider = new providers.JsonRpcProvider({ url: network.nodeUrl }); + + // Retrieve transaction result from the network + const transaction = await provider.txStatus(txhash, 'unnused'); + return providers.getTransactionLastResult(transaction); + }; + + getBalance = async (accountId: string) => { + const walletSelector = await this.selector; + const { network } = walletSelector.options; + const provider = new providers.JsonRpcProvider({ url: network.nodeUrl }); + + // Retrieve account state from the network + const account = await provider.query({ + request_type: 'view_account', + account_id: accountId, + finality: 'final', + }); + + // return amount on NEAR + return account.amount ? utils.format.formatNearAmount(account.amount) : 0; + }; +} + +export const NearContext: Context<{ wallet: Wallet | undefined; signedAccountId: string }> = createContext({ + wallet: undefined as Wallet | undefined, + signedAccountId: '', +}); diff --git a/src/components/marketing-navigation/UserDropdownMenu.tsx b/src/components/marketing-navigation/UserDropdownMenu.tsx index 78d729b0d..8b3958b1b 100644 --- a/src/components/marketing-navigation/UserDropdownMenu.tsx +++ b/src/components/marketing-navigation/UserDropdownMenu.tsx @@ -9,6 +9,9 @@ import { useAuthStore } from '@/stores/auth'; import { useVmStore } from '@/stores/vm'; import { NftImage } from '../NTFImage'; +import { useContext } from 'react'; +import { NearContext } from '../WalletSelector'; +import { signInContractId } from '@/utils/config'; const Wrapper = styled.div` flex-grow: 1; @@ -92,30 +95,32 @@ type Props = { }; export const UserDropdownMenu = ({ collapsed }: Props) => { - const accountId = useAuthStore((store) => store.accountId); - const availableStorage = useAuthStore((store) => store.availableStorage); - const logOut = useAuthStore((store) => store.logOut); - const near = useVmStore((store) => store.near); + const { wallet, signedAccountId } = useContext(NearContext); + const availableStorage: bigint = useAuthStore((store) => store.availableStorage); const router = useRouter(); const components = useBosComponents(); const withdrawStorage = useCallback(async () => { - if (!near) return; - await near.contract.storage_withdraw({}, undefined, '1'); - }, [near]); + if (!wallet) return; + await wallet.callMethod({ contractId: signInContractId, method: 'storage_withdraw', deposit: '1' }); + }, [wallet]); const [profile, setProfile] = useState({}); useEffect(() => { async function getProfile() { - const profile = await near.viewCall('social.near', 'get', { keys: [`${accountId}/profile/**`] }); - setProfile(profile[accountId].profile); + const profile = await wallet.viewMethod({ + contractId: 'social.near', + method: 'get', + args: { keys: [`${signedAccountId}/profile/**`] }, + }); + setProfile(profile[signedAccountId].profile); } - if (!near || !accountId) return; + if (!wallet || !signedAccountId) return; getProfile(); - }, [near, accountId]); + }, [wallet, signedAccountId]); return ( @@ -126,33 +131,35 @@ export const UserDropdownMenu = ({ collapsed }: Props) => { ) : ( - +
{profile.name}
-
{accountId}
+
{signedAccountId}
)} - router.push(`/${components.profilePage}?accountId=${accountId}`)}> + router.push(`/${components.profilePage}?accountId=${signedAccountId}`)}> } /> Profile - router.push(`/settings`)}> - } /> - Settings - router.push(`/wallet-utilities`)}> } /> Wallet Utilities - withdrawStorage()}> - } /> - {availableStorage && `Withdraw ${availableStorage.div(1000).toFixed(2)}kb`} - - logOut()}> + {availableStorage && availableStorage > BigInt(0) && ( + withdrawStorage()}> + } /> + {`Withdraw ${availableStorage / BigInt(1000)}kb`} + + )} + wallet.signOut()}> } /> Sign out diff --git a/src/components/sidebar-navigation/Sidebar.tsx b/src/components/sidebar-navigation/Sidebar.tsx index f01bd3f52..0b8d77cbd 100644 --- a/src/components/sidebar-navigation/Sidebar.tsx +++ b/src/components/sidebar-navigation/Sidebar.tsx @@ -12,6 +12,8 @@ import { Search } from './Search'; import { useNavigationStore } from './store'; import * as S from './styles'; import { currentPathMatchesRoute } from './utils'; +import { useContext } from 'react'; +import { NearContext } from '../WalletSelector'; export const Sidebar = () => { const router = useRouter(); @@ -21,7 +23,7 @@ export const Sidebar = () => { const toggleExpandedSidebar = useNavigationStore((store) => store.toggleExpandedSidebar); const handleBubbledClickInSidebar = useNavigationStore((store) => store.handleBubbledClickInSidebar); const tooltipsDisabled = isSidebarExpanded; - const signedIn = useAuthStore((store) => store.signedIn); + const { signedAccountId } = useContext(NearContext); const { requestAuthentication } = useSignInRedirect(); const inputRef = useRef(null); @@ -182,7 +184,7 @@ export const Sidebar = () => { - {signedIn ? ( + {signedAccountId ? ( ) : ( diff --git a/src/components/vm/VM.tsx b/src/components/vm/VM.tsx new file mode 100644 index 000000000..7a7be574a --- /dev/null +++ b/src/components/vm/VM.tsx @@ -0,0 +1,99 @@ +import { isValidAttribute } from 'dompurify'; +import { mapValues } from 'lodash'; +import { EthersProviderContext, useInitNear, Widget, utils, useAccount } from 'near-social-vm'; +import Link from 'next/link'; +import React, { useEffect, useContext, useState } from 'react'; + +import { useEthersProviderContext } from '@/data/web3'; +import { cookiePreferences, optOut, recordHandledError, recordWalletConnect } from '@/utils/analytics'; +import { commitModalBypassAuthorIds, commitModalBypassSources, isLocalEnvironment, networkId } from '@/utils/config'; +import { NearContext } from '../WalletSelector'; +import { useAuthStore } from '@/stores/auth'; + +export default function Component(props: any) { + const ethersContext = useEthersProviderContext(); + const { wallet } = useContext(NearContext); + const { initNear } = useInitNear(); + const account = useAccount(); + const setAuthStore = useAuthStore((state: any) => state.set); + const [availableStorage, setAvailableStorage] = useState(null); + + useEffect(() => { + wallet && + wallet.selector && + initNear && + initNear({ + networkId, + walletConnectCallback: recordWalletConnect, + errorCallback: recordHandledError, + selector: wallet.selector, + customElements: { + Link: ({ to, href, ...rest }: { to: string | object | undefined; href: string | object }) => { + const cleanProps = mapValues({ to, href, ...rest }, (val: any, key: string) => { + if (!['to', 'href'].includes(key)) return val; + if (key === 'href' && !val) val = to; + const isAtrValid = typeof val === 'string' && isValidAttribute('a', 'href', val); + let linkValue; + if (isAtrValid) { + if (val.startsWith('?')) { + const currentParams = new URLSearchParams( + Object.entries(router.query).reduce((acc, [key, value]) => { + if (typeof value === 'string') { + acc[key] = value; + } + return acc; + }, {} as { [key: string]: string }), // Add index signature to the object type + ); + const newParams = new URLSearchParams(val); + newParams.forEach((value, key) => { + currentParams.set(key, value); + }); + + linkValue = `?${currentParams.toString()}`; + } else { + linkValue = val; + } + } else { + linkValue = 'about:blank'; + } + return linkValue; + }); + return ; + }, + AnalyticsCookieConsent: ({ all, onlyRequired }: { all: boolean; onlyRequired: boolean }) => { + localStorage.setItem('cookiesAcknowledged', all ? cookiePreferences.all : cookiePreferences.onlyRequired); + optOut(onlyRequired); + return <>; + }, + }, + features: { + commitModalBypass: { + authorIds: commitModalBypassAuthorIds, + sources: commitModalBypassSources, + }, + enableComponentSrcDataKey: true, + enableWidgetSrcWithCodeOverride: isLocalEnvironment, + }, + }); + }, [wallet, initNear]); + + useEffect(() => { + setAvailableStorage( + account.storageBalance ? BigInt(account.storageBalance.available) / BigInt(utils.StorageCostPerByte) : BigInt(0), + ); + }, [account]); + + useEffect(() => { + setAuthStore({ + availableStorage, + }); + }, [availableStorage]); + + return ( +
+ + + +
+ ); +} diff --git a/src/components/vm/VmCommitButton.tsx b/src/components/vm/VmCommitButton.tsx deleted file mode 100644 index cd537ff6f..000000000 --- a/src/components/vm/VmCommitButton.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Placeholder } from '@near-pagoda/ui'; - -import { useVmStore } from '@/stores/vm'; - -type Props = { - className?: string; - data: Record; - handleCommit?: () => void; - onCommit?: () => void; -}; - -export function VmCommitButton(props: Props) { - const { near, CommitButton } = useVmStore(); - - if (!near || !CommitButton) { - return ; - } - - return ; -} diff --git a/src/components/vm/VmComponent.tsx b/src/components/vm/VmComponent.tsx index 70e5ef244..b2f32a18a 100644 --- a/src/components/vm/VmComponent.tsx +++ b/src/components/vm/VmComponent.tsx @@ -1,5 +1,11 @@ +import dynamic from 'next/dynamic'; + import { useBosLoaderStore } from '@/stores/bos-loader'; -import { useVmStore } from '@/stores/vm'; + +const Component = dynamic(() => import('./VM'), { + ssr: false, + loading: () =>

Loading ...

, +}); type Props = { src: string; @@ -7,21 +13,16 @@ type Props = { }; export function VmComponent(props: Props) { - const { EthersProvider, ethersContext, Widget } = useVmStore(); const redirectMapStore = useBosLoaderStore(); - if (!EthersProvider || !redirectMapStore.hasResolved) { - return null; - } - return ( - - + - + ); } diff --git a/src/components/vm/VmInitializer.tsx b/src/components/vm/VmInitializer.tsx deleted file mode 100644 index fa36fd188..000000000 --- a/src/components/vm/VmInitializer.tsx +++ /dev/null @@ -1,327 +0,0 @@ -import { setupKeypom } from '@keypom/selector'; -import type { WalletSelector } from '@near-wallet-selector/core'; -import { setupWalletSelector } from '@near-wallet-selector/core'; -import { setupHereWallet } from '@near-wallet-selector/here-wallet'; -import { setupLedger } from '@near-wallet-selector/ledger'; -import { setupMeteorWallet } from '@near-wallet-selector/meteor-wallet'; -import { setupMintbaseWallet } from '@near-wallet-selector/mintbase-wallet'; -import type { WalletSelectorModal } from '@near-wallet-selector/modal-ui'; -import { setupModal } from '@near-wallet-selector/modal-ui'; -import { setupMyNearWallet } from '@near-wallet-selector/my-near-wallet'; -import { setupNearMobileWallet } from '@near-wallet-selector/near-mobile-wallet'; -import { setupNeth } from '@near-wallet-selector/neth'; -import { setupNightly } from '@near-wallet-selector/nightly'; -import { setupSender } from '@near-wallet-selector/sender'; -import { setupWelldoneWallet } from '@near-wallet-selector/welldone-wallet'; -import Big from 'big.js'; -import { isValidAttribute } from 'dompurify'; -import { mapValues } from 'lodash'; -import { setupFastAuthWallet } from 'near-fastauth-wallet'; -import { - CommitButton, - EthersProviderContext, - useAccount, - useCache, - useInitNear, - useNear, - utils, - Widget, -} from 'near-social-vm'; -import Link from 'next/link'; -import { useRouter } from 'next/router'; -import React, { useCallback, useEffect, useState } from 'react'; - -import { useEthersProviderContext } from '@/data/web3'; -import { useIdOS } from '@/hooks/useIdOS'; -import { useSignInRedirect } from '@/hooks/useSignInRedirect'; -import { useAuthStore } from '@/stores/auth'; -import { useIdosStore } from '@/stores/idosStore'; -import { useVmStore } from '@/stores/vm'; -import { - cookiePreferences, - optOut, - recordHandledError, - recordWalletConnect, - reset as resetAnalytics, -} from '@/utils/analytics'; -import { - commitModalBypassAuthorIds, - commitModalBypassSources, - isLocalEnvironment, - networkId, - signInContractId, -} from '@/utils/config'; -import { KEYPOM_OPTIONS } from '@/utils/keypom-options'; - -import { useNavigationStore } from '../sidebar-navigation/store'; - -export default function VmInitializer() { - const [signedIn, setSignedIn] = useState(false); - const [signedAccountId, setSignedAccountId] = useState(null); - const [availableStorage, setAvailableStorage] = useState(null); - const [walletModal, setWalletModal] = useState(null); - const ethersProviderContext = useEthersProviderContext(); - const { initNear } = useInitNear(); - const near = useNear(); - const account = useAccount(); - const cache = useCache(); - const accountId = account.accountId; - const setAuthStore = useAuthStore((state) => state.set); - const setVmStore = useVmStore((store) => store.set); - const { requestAuthentication, saveCurrentUrl } = useSignInRedirect(); - const idOS = useIdOS(); - const idosSDK = useIdosStore((state) => state.idOS); - const resetNavigation = useNavigationStore((store) => store.reset); - const router = useRouter(); - - useEffect(() => { - initNear && - initNear({ - networkId, - walletConnectCallback: recordWalletConnect, - errorCallback: recordHandledError, - selector: setupWalletSelector({ - network: networkId, - modules: [ - setupMyNearWallet(), - setupSender(), - setupHereWallet(), - setupMintbaseWallet(), - setupMeteorWallet(), - setupNeth({ - gas: '300000000000000', - bundle: false, - }), - setupNightly(), - setupWelldoneWallet(), - setupFastAuthWallet({ - walletUrl: - networkId === 'testnet' - ? 'https://wallet.testnet.near.org/fastauth' - : 'https://wallet.near.org/fastauth', - relayerUrl: - networkId === 'testnet' - ? 'http://34.70.226.83:3030/relay' - : 'https://near-relayer-mainnet.api.pagoda.co/relay', - }), - setupKeypom({ - trialAccountSpecs: { - url: - networkId == 'testnet' - ? 'https://test.near.org/#trial-url/ACCOUNT_ID/SECRET_KEY' - : 'https://dev.near.org/#trial-url/ACCOUNT_ID/SECRET_KEY', - modalOptions: KEYPOM_OPTIONS(networkId), - }, - instantSignInSpecs: { - url: - networkId == 'testnet' - ? 'https://test.near.org/#instant-url/ACCOUNT_ID/SECRET_KEY/MODULE_ID' - : 'https://dev.near.org/#instant-url/ACCOUNT_ID/SECRET_KEY/MODULE_ID', - }, - networkId, - signInContractId, - }) as any, // TODO: Refactor setupKeypom() to TS - setupLedger(), - setupNearMobileWallet(), - ], - }), - customElements: { - Link: ({ to, href, ...rest }: { to: string | object | undefined; href: string | object }) => { - const cleanProps = mapValues({ to, href, ...rest }, (val: any, key: string) => { - if (!['to', 'href'].includes(key)) return val; - if (key === 'href' && !val) val = to; - const isAtrValid = typeof val === 'string' && isValidAttribute('a', 'href', val); - let linkValue; - if (isAtrValid) { - if (val.startsWith('?')) { - const currentParams = new URLSearchParams( - Object.entries(router.query).reduce((acc, [key, value]) => { - if (typeof value === 'string') { - acc[key] = value; - } - return acc; - }, {} as { [key: string]: string }), // Add index signature to the object type - ); - const newParams = new URLSearchParams(val); - newParams.forEach((value, key) => { - currentParams.set(key, value); - }); - - linkValue = `?${currentParams.toString()}`; - } else { - linkValue = val; - } - } else { - linkValue = 'about:blank'; - } - return linkValue; - }); - - return ; - }, - AnalyticsCookieConsent: ({ all, onlyRequired }: { all: boolean; onlyRequired: boolean }) => { - localStorage.setItem('cookiesAcknowledged', all ? cookiePreferences.all : cookiePreferences.onlyRequired); - optOut(onlyRequired); - return <>; - }, - }, - features: { - commitModalBypass: { - authorIds: commitModalBypassAuthorIds, - sources: commitModalBypassSources, - }, - enableComponentSrcDataKey: true, - enableWidgetSrcWithCodeOverride: isLocalEnvironment, - }, - }); - }, [initNear, router.query]); - - useEffect(() => { - if (!near || !idOS) { - return; - } - near.selector.then((selector: WalletSelector) => { - const selectorModal = setupModal(selector, { - contractId: near.config.contractName, - methodNames: idOS.near.contractMethods, - }); - setWalletModal(selectorModal); - }); - }, [idOS, near]); - - const requestSignMessage = useCallback( - async (message: string) => { - if (!near) { - return; - } - const wallet = await (await near.selector).wallet(); - const nonce = Buffer.from(Array.from(Array(32).keys())); - const recipient = 'social.near'; - - try { - const signedMessage = await wallet.signMessage({ - message, - nonce, - recipient, - }); - - if (signedMessage) { - const verifiedFullKeyBelongsToUser = await wallet.verifyOwner({ - message: signedMessage, - }); - - if (verifiedFullKeyBelongsToUser) { - alert(`Successfully verify signed message: '${message}': \n ${JSON.stringify(signedMessage)}`); - } else { - alert(`Failed to verify signed message '${message}': \n ${JSON.stringify(signedMessage)}`); - } - } - } catch (err) { - const errMsg = err instanceof Error ? err.message : 'Something went wrong'; - alert(errMsg); - recordHandledError({ scope: 'requestSignMessage', message: errMsg }); - } - }, - [near], - ); - - const requestSignInWithWallet = useCallback(() => { - saveCurrentUrl(); - walletModal?.show(); - return false; - }, [saveCurrentUrl, walletModal]); - - useEffect(() => { - const handleShowWalletSelector = (e: MessageEvent<{ showWalletSelector: boolean }>) => { - if (e.data.showWalletSelector) { - requestSignInWithWallet(); - } - }; - window.addEventListener('message', handleShowWalletSelector, false); - return () => { - window.removeEventListener('message', handleShowWalletSelector, false); - }; - }, [requestSignInWithWallet]); - - const logOut = useCallback(async () => { - if (!near) { - return; - } - await idosSDK?.reset({ enclave: true }); - useIdosStore.persist.clearStorage(); - const wallet = await (await near.selector).wallet(); - wallet.signOut(); - near.accountId = null; - setSignedIn(false); - setSignedAccountId(null); - resetAnalytics(); - resetNavigation(); - }, [idosSDK, near, resetNavigation]); - - const refreshAllowance = useCallback(async () => { - alert("You're out of access key allowance. Need sign in again to refresh it"); - await logOut(); - requestAuthentication(); - }, [logOut, requestAuthentication]); - - useEffect(() => { - if (!near) { - return; - } - setSignedIn(!!accountId); - setSignedAccountId(accountId); - }, [near, accountId]); - - useEffect(() => { - setAvailableStorage( - account.storageBalance ? Big(account.storageBalance.available).div(utils.StorageCostPerByte) : Big(0), - ); - }, [account]); - - useEffect(() => { - if (navigator.userAgent !== 'ReactSnap') { - const pageFlashPrevent = document.getElementById('page-flash-prevent'); - if (pageFlashPrevent) { - pageFlashPrevent.remove(); - } - } - }, []); - - useEffect(() => { - setAuthStore({ - account, - accountId: signedAccountId || '', - availableStorage, - logOut, - refreshAllowance, - requestSignInWithWallet, - requestSignMessage, - vmNear: near, - signedIn, - }); - }, [ - account, - availableStorage, - logOut, - refreshAllowance, - requestSignInWithWallet, - requestSignMessage, - signedIn, - signedAccountId, - setAuthStore, - near, - ]); - - useEffect(() => { - setVmStore({ - cache, - CommitButton, - ethersContext: ethersProviderContext, - EthersProvider: EthersProviderContext.Provider, - Widget, - near, - }); - }, [cache, ethersProviderContext, setVmStore, near]); - - return <>; -} diff --git a/src/components/wallet-utilities/SendNear.tsx b/src/components/wallet-utilities/SendNear.tsx index 882b50907..3e85b2360 100644 --- a/src/components/wallet-utilities/SendNear.tsx +++ b/src/components/wallet-utilities/SendNear.tsx @@ -4,7 +4,8 @@ import { useEffect, useState } from 'react'; import type { SubmitHandler } from 'react-hook-form'; import { useForm } from 'react-hook-form'; -import { useAuthStore } from '@/stores/auth'; +import { useContext } from 'react'; +import { NearContext } from '../WalletSelector'; type FormData = { sendNearAmount: number; @@ -25,27 +26,27 @@ function displayBalance(balance: number) { export const SendNear = () => { const form = useForm(); - const accountId = useAuthStore((store) => store.accountId); - const wallet = useAuthStore((store) => store.wallet); - const near = useAuthStore((store) => store.vmNear); + const { wallet, signedAccountId } = useContext(NearContext); + // const accountId = useAuthStore((store) => store.accountId); + // const wallet = useAuthStore((store) => store.wallet); + // const near = useAuthStore((store) => store.vmNear); const [currentNearAmount, setCurrentNearAmount] = useState(0); useEffect(() => { - if (!near || !accountId) return; + if (!wallet || !signedAccountId) return; const loadBalance = async () => { try { - const state = await near.accountState(accountId); + const balance = await wallet.getBalance(signedAccountId); const requiredGas = 0.00005; - const balance = Number(utils.format.formatNearAmount(state.amount || '0', 5)) - requiredGas; - setCurrentNearAmount(balance); + setCurrentNearAmount(balance - requiredGas); } catch (error) { console.error(error); } }; loadBalance(); - }, [accountId, near]); + }, [wallet, signedAccountId]); const validSubmitHandler: SubmitHandler = async (data) => { try { @@ -62,7 +63,7 @@ export const SendNear = () => { type: 'Transfer', }, ], - signerId: accountId, + signerId: signedAccountId, receiverId: data.sendToAccountId, }); diff --git a/src/hooks/useSignInRedirect.ts b/src/hooks/useSignInRedirect.ts index 760a43607..fa92f96b4 100644 --- a/src/hooks/useSignInRedirect.ts +++ b/src/hooks/useSignInRedirect.ts @@ -1,11 +1,13 @@ import { useRouter } from 'next/router'; import { useCallback } from 'react'; -import { useAuthStore } from '@/stores/auth'; +import { useContext } from 'react'; +import { NearContext } from '@/components/WalletSelector'; +import { signInContractId } from '@/utils/config'; export function useSignInRedirect() { const router = useRouter(); - const vmNear = useAuthStore((store) => store.vmNear); + const { wallet } = useContext(NearContext); const redirect = useCallback( (hardRefresh = false) => { @@ -32,17 +34,17 @@ export function useSignInRedirect() { const requestAuthentication = useCallback( (createAccount = false) => { saveCurrentUrl(); - if (!vmNear) return; - vmNear.selector + if (!wallet) return; + wallet.selector .then((selector: any) => selector.wallet('fast-auth-wallet')) .then((fastAuthWallet: any) => fastAuthWallet.signIn({ - contractId: vmNear.config.contractName, + contractId: signInContractId, isRecovery: !createAccount, }), ); }, - [saveCurrentUrl, vmNear], + [saveCurrentUrl, wallet], ); return { diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 00a7e7ead..a92fb0ebf 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -10,7 +10,6 @@ import 'react-bootstrap-typeahead/css/Typeahead.bs5.css'; import { openToast, Toaster } from '@near-pagoda/ui'; import Gleap from 'gleap'; import type { AppProps } from 'next/app'; -import dynamic from 'next/dynamic'; import Head from 'next/head'; import { useRouter } from 'next/router'; import Script from 'next/script'; @@ -24,14 +23,13 @@ import { useHashUrlBackwardsCompatibility } from '@/hooks/useHashUrlBackwardsCom import { usePageAnalytics } from '@/hooks/usePageAnalytics'; import { useAuthStore } from '@/stores/auth'; import { init as initializeAnalytics, recordHandledError, setReferrer } from '@/utils/analytics'; -import { gleapSdkToken } from '@/utils/config'; import { setNotificationsLocalStorage } from '@/utils/notificationsLocalStorage'; import type { NextPageWithLayout } from '@/utils/types'; import { styleZendesk } from '@/utils/zendesk'; +import { Wallet, NearContext } from '@/components/WalletSelector'; -const VmInitializer = dynamic(() => import('../components/vm/VmInitializer'), { - ssr: false, -}); +import { gleapSdkToken, networkId, signInContractId } from '@/utils/config'; +import { useState } from 'react'; type AppPropsWithLayout = AppProps & { Component: NextPageWithLayout; @@ -41,6 +39,8 @@ if (typeof window !== 'undefined') { if (gleapSdkToken) Gleap.initialize(gleapSdkToken); } +const wallet = new Wallet({ createAccessKeyFor: signInContractId, networkId: networkId }); + export default function App({ Component, pageProps }: AppPropsWithLayout) { useBosLoaderInitializer(); useHashUrlBackwardsCompatibility(); @@ -48,7 +48,11 @@ export default function App({ Component, pageProps }: AppPropsWithLayout) { useClickTracking(); const getLayout = Component.getLayout ?? ((page) => page); const router = useRouter(); - const signedIn = useAuthStore((store) => store.signedIn); + const [signedAccountId, setSignedAccountId] = useState(''); + + useEffect(() => { + wallet.startUp(setSignedAccountId); + }, []); useEffect(() => { const referred_from_wallet = document.referrer.indexOf('https://wallet.near.org/') !== -1; @@ -67,10 +71,10 @@ export default function App({ Component, pageProps }: AppPropsWithLayout) { useEffect(() => { // this check is needed to init localStorage for notifications after user signs in - if (signedIn) { + if (signedAccountId) { setNotificationsLocalStorage(); } - }, [signedIn]); + }, [signedAccountId]); useEffect(() => { router.events.on('routeChangeStart', () => { @@ -102,8 +106,24 @@ export default function App({ Component, pageProps }: AppPropsWithLayout) { }; }, []); + // needed by fast auth to show the wallet selector when the user chooses "use a wallet" + useEffect(() => { + if (!wallet) return; + + const handleShowWalletSelector = (e: MessageEvent<{ showWalletSelector: boolean }>) => { + if (e.data.showWalletSelector) { + wallet.signIn(); + } + }; + + window.addEventListener('message', handleShowWalletSelector, false); + return () => { + window.removeEventListener('message', handleShowWalletSelector, false); + }; + }, [wallet]); + return ( - <> + @@ -115,8 +135,6 @@ export default function App({ Component, pageProps }: AppPropsWithLayout) {