Smart wallet auth (#3220)
* basic login button implementation * refactoring and add dynamic auth button to the other pages * connect wallet without authenticating an user * login and logout user in blockscout account * refactoring and RPC overrides * authenticate user with connected wallet * add ENS to profile button * override evm chains and fix contract write with waas wallet * disconnect wallet and fix docker build * add dynamic imports to components that use web3 wallet packages * refactor user profile directory * clean up * tweaks * update pw ci script * fix "connect wallet" button behavior * handle linking email to profile with dynamic provider * fix test * async loading web3 hooks and remove dynamic loading from components that use them * fix pw tests * add account navigation in horizontal layout when auth provider is dynamic * fix address verification modal * review fixes * spelling fixes
@@ -98,7 +98,7 @@ RUN set -a && \
|
||||
# ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
||||
# Build app for production
|
||||
ENV NODE_OPTIONS="--max-old-space-size=4096"
|
||||
ENV NODE_OPTIONS="--max-old-space-size=8192"
|
||||
RUN yarn build
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Feature } from './types';
|
||||
import type { AuthProvider } from 'types/client/account';
|
||||
|
||||
import app from '../app';
|
||||
import services from '../services';
|
||||
@@ -6,18 +7,39 @@ import { getEnvValue } from '../utils';
|
||||
|
||||
const title = 'My account';
|
||||
|
||||
const config: Feature<{ isEnabled: true; recaptchaSiteKey: string }> = (() => {
|
||||
const config: Feature<{
|
||||
isEnabled: true;
|
||||
authProvider: AuthProvider;
|
||||
dynamic?: {
|
||||
environmentId: string;
|
||||
};
|
||||
}> = (() => {
|
||||
|
||||
if (
|
||||
!app.isPrivateMode &&
|
||||
getEnvValue('NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED') === 'true' &&
|
||||
services.reCaptchaV2.siteKey
|
||||
getEnvValue('NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED') === 'true'
|
||||
) {
|
||||
return Object.freeze({
|
||||
title,
|
||||
isEnabled: true,
|
||||
recaptchaSiteKey: services.reCaptchaV2.siteKey,
|
||||
});
|
||||
const authProvider = getEnvValue('NEXT_PUBLIC_ACCOUNT_AUTH_PROVIDER');
|
||||
const dynamicEnvironmentId = getEnvValue('NEXT_PUBLIC_ACCOUNT_DYNAMIC_ENVIRONMENT_ID');
|
||||
|
||||
if (authProvider === 'dynamic' && dynamicEnvironmentId) {
|
||||
return Object.freeze({
|
||||
title,
|
||||
isEnabled: true,
|
||||
authProvider: 'dynamic',
|
||||
dynamic: {
|
||||
environmentId: dynamicEnvironmentId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (services.reCaptchaV2.siteKey) {
|
||||
return Object.freeze({
|
||||
title,
|
||||
isEnabled: true,
|
||||
authProvider: 'auth0',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return Object.freeze({
|
||||
|
||||
@@ -3,13 +3,22 @@ import type { Feature } from './types';
|
||||
import app from '../app';
|
||||
import chain from '../chain';
|
||||
import { getEnvValue, parseEnvJson } from '../utils';
|
||||
import accountFeature from './account';
|
||||
import opSuperchain from './opSuperchain';
|
||||
|
||||
const walletConnectProjectId = getEnvValue('NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID');
|
||||
|
||||
const title = 'Blockchain interaction (writing to contract, etc.)';
|
||||
|
||||
const config: Feature<{ walletConnect: { projectId: string; featuredWalletIds: Array<string> } }> = (() => {
|
||||
type FeaturePayload = {
|
||||
connectorType: 'reown';
|
||||
reown: { projectId: string; featuredWalletIds: Array<string> };
|
||||
} | {
|
||||
connectorType: 'dynamic';
|
||||
dynamic: { environmentId: string };
|
||||
};
|
||||
|
||||
const config: Feature<FeaturePayload> = (() => {
|
||||
|
||||
// all chain parameters are required for wagmi provider
|
||||
// @wagmi/chains/dist/index.d.ts
|
||||
@@ -26,17 +35,28 @@ const config: Feature<{ walletConnect: { projectId: string; featuredWalletIds: A
|
||||
|
||||
if (
|
||||
!app.isPrivateMode &&
|
||||
(isSingleChain || isOpSuperchain) &&
|
||||
walletConnectProjectId
|
||||
(isSingleChain || isOpSuperchain)
|
||||
) {
|
||||
return Object.freeze({
|
||||
title,
|
||||
isEnabled: true,
|
||||
walletConnect: {
|
||||
projectId: walletConnectProjectId,
|
||||
featuredWalletIds: parseEnvJson<Array<string>>(getEnvValue('NEXT_PUBLIC_WALLET_CONNECT_FEATURED_WALLET_IDS')) ?? [],
|
||||
},
|
||||
});
|
||||
if (accountFeature.isEnabled && accountFeature.authProvider === 'dynamic' && accountFeature.dynamic?.environmentId) {
|
||||
return Object.freeze({
|
||||
title,
|
||||
isEnabled: true,
|
||||
connectorType: 'dynamic',
|
||||
dynamic: {
|
||||
environmentId: accountFeature.dynamic.environmentId,
|
||||
},
|
||||
});
|
||||
} else if (walletConnectProjectId) {
|
||||
return Object.freeze({
|
||||
title,
|
||||
isEnabled: true,
|
||||
connectorType: 'reown',
|
||||
reown: {
|
||||
projectId: walletConnectProjectId,
|
||||
featuredWalletIds: parseEnvJson<Array<string>>(getEnvValue('NEXT_PUBLIC_WALLET_CONNECT_FEATURED_WALLET_IDS')) ?? [],
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return Object.freeze({
|
||||
|
||||
@@ -8,7 +8,8 @@ import blockchainInteraction from './blockchainInteraction';
|
||||
const title = 'Rewards service integration';
|
||||
|
||||
const config: Feature<{}> = (() => {
|
||||
if (!app.isPrivateMode && apis.rewards && account.isEnabled && blockchainInteraction.isEnabled) {
|
||||
// @0xdeval: as of now, we won't support rewards programs with dynamic auth provider
|
||||
if (!app.isPrivateMode && apis.rewards && account.isEnabled && account.authProvider === 'auth0' && blockchainInteraction.isEnabled) {
|
||||
return Object.freeze({
|
||||
title,
|
||||
isEnabled: true,
|
||||
|
||||
@@ -10,6 +10,7 @@ NEXT_PUBLIC_APP_ENV=development
|
||||
NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws
|
||||
|
||||
NEXT_PUBLIC_HOMEPAGE_HIGHLIGHTS_CONFIG=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/homepage-highlights/test.json
|
||||
# NEXT_PUBLIC_ACCOUNT_AUTH_PROVIDER=dynamic
|
||||
|
||||
# Instance ENVs
|
||||
NEXT_PUBLIC_AD_BANNER_ENABLE_SPECIFY=true
|
||||
@@ -64,7 +65,7 @@ NEXT_PUBLIC_NETWORK_ID=11155111
|
||||
NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/sepolia.svg
|
||||
NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/sepolia.svg
|
||||
NEXT_PUBLIC_NETWORK_NAME=Sepolia
|
||||
NEXT_PUBLIC_NETWORK_RPC_URL=https://sepolia.drpc.org
|
||||
NEXT_PUBLIC_NETWORK_RPC_URL=https://ethereum-sepolia-rpc.publicnode.com
|
||||
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation
|
||||
NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true
|
||||
NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/sepolia-testnet.png
|
||||
|
||||
@@ -25,7 +25,9 @@
|
||||
// Specify publisher key
|
||||
"spk_\\w+",
|
||||
// Posthog project key
|
||||
"phc_\\w+"
|
||||
"phc_\\w+",
|
||||
// GitHub account names
|
||||
"@(\\w|-)+",
|
||||
],
|
||||
// words - list of words to be always considered correct
|
||||
"words": [
|
||||
@@ -105,7 +107,6 @@
|
||||
"explorable",
|
||||
"facebookexternalhit",
|
||||
"favicons",
|
||||
"fedoseev",
|
||||
"filecoin",
|
||||
"flashblock",
|
||||
"flashblocks",
|
||||
@@ -131,7 +132,6 @@
|
||||
"inpage",
|
||||
"internaltx",
|
||||
"ipfs",
|
||||
"isstuev",
|
||||
"iszero",
|
||||
"jazzicon",
|
||||
"jfif",
|
||||
@@ -252,7 +252,6 @@
|
||||
"utka",
|
||||
"utko",
|
||||
"UUPS",
|
||||
"vbaranov",
|
||||
"VCALENDAR",
|
||||
"vercel",
|
||||
"verifreg",
|
||||
|
||||
@@ -103,7 +103,6 @@ const schema = yup
|
||||
NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED: yup.boolean(),
|
||||
NEXT_PUBLIC_ADVANCED_FILTER_ENABLED: yup.boolean(),
|
||||
NEXT_PUBLIC_CELO_ENABLED: yup.boolean(),
|
||||
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED: yup.boolean(),
|
||||
NEXT_PUBLIC_DEX_POOLS_ENABLED: yup.boolean()
|
||||
.when('NEXT_PUBLIC_CONTRACT_INFO_API_HOST', {
|
||||
is: (value: string) => Boolean(value),
|
||||
@@ -159,6 +158,7 @@ const schema = yup
|
||||
.concat(uiSchemas.footerSchema)
|
||||
.concat(uiSchemas.miscSchema)
|
||||
.concat(uiSchemas.viewsSchema)
|
||||
.concat(featuresSchemas.accountSchema)
|
||||
.concat(featuresSchemas.address3rdPartyWidgetsConfigSchema)
|
||||
.concat(featuresSchemas.adsSchema)
|
||||
.concat(featuresSchemas.apiDocsSchema)
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { AuthProvider } from 'types/client/account';
|
||||
import * as yup from 'yup';
|
||||
|
||||
export const accountSchema = yup
|
||||
.object()
|
||||
.shape({
|
||||
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED: yup.boolean(),
|
||||
NEXT_PUBLIC_ACCOUNT_AUTH_PROVIDER: yup
|
||||
.string<AuthProvider>()
|
||||
.when('NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED', {
|
||||
is: (value: boolean) => value === true,
|
||||
then: (schema) => schema.oneOf([ 'auth0', 'dynamic' ]),
|
||||
otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_ACCOUNT_AUTH_PROVIDER cannot not be used if NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED is not defined'),
|
||||
}),
|
||||
NEXT_PUBLIC_ACCOUNT_DYNAMIC_ENVIRONMENT_ID: yup
|
||||
.string()
|
||||
.when('NEXT_PUBLIC_ACCOUNT_AUTH_PROVIDER', {
|
||||
is: (value: AuthProvider) => value === 'dynamic',
|
||||
then: (schema) => schema.required(),
|
||||
otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_ACCOUNT_DYNAMIC_ENVIRONMENT_ID can only be used if NEXT_PUBLIC_ACCOUNT_AUTH_PROVIDER is set to \'dynamic\' '),
|
||||
}),
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './account';
|
||||
export * from './address3rdPartyWidgets';
|
||||
export * from './ads';
|
||||
export * from './apiDocs';
|
||||
|
||||
@@ -10,3 +10,6 @@ NEXT_PUBLIC_METADATA_ADDRESS_TAGS_UPDATE_ENABLED=false
|
||||
NEXT_PUBLIC_HAS_USER_OPS=true
|
||||
NEXT_PUBLIC_USER_OPS_INDEXER_API_HOST=https://example.com
|
||||
NEXT_PUBLIC_NAVIGATION_PROMO_BANNER_CONFIG={'img_url': {'small': 'https://example.com/promo-sm.png', 'large': 'https://example.com/promo-lg.png'}, 'link_url': 'https://example.com'}
|
||||
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
|
||||
NEXT_PUBLIC_ACCOUNT_AUTH_PROVIDER=dynamic
|
||||
NEXT_PUBLIC_ACCOUNT_DYNAMIC_ENVIRONMENT_ID=xxx
|
||||
@@ -428,7 +428,9 @@ Settings for meta tags, OG tags and SEO
|
||||
| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED | `boolean` | Set to true if network has account feature | Required | - | `true` | v1.0.x+ |
|
||||
| NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY | `boolean` | See [below](#google-recaptcha) | Required | - | `<your-secret>` | v1.0.x+ |
|
||||
| NEXT_PUBLIC_ACCOUNT_AUTH_PROVIDER | `auth0 \| dynamic` | Auth provider that enables basic user authentication. | - | `auth0` | `dynamic` | upcoming |
|
||||
| NEXT_PUBLIC_ACCOUNT_DYNAMIC_ENVIRONMENT_ID | `string` | Environment ID of the Dynamic project. | Required, if provider is `dynamic` | - | `<your-secret>` | upcoming |
|
||||
| NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY | `boolean` | See [below](#google-recaptcha) | Required, if provided is `auth0` | - | `<your-secret>` | v1.0.x+ |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -58,6 +58,9 @@ export const GENERAL_API_ACCOUNT_RESOURCES = {
|
||||
auth_logout: {
|
||||
path: '/api/account/auth/logout',
|
||||
},
|
||||
auth_dynamic: {
|
||||
path: '/api/account/v2/authenticate_via_dynamic',
|
||||
},
|
||||
} satisfies Record<string, ApiResource>;
|
||||
|
||||
export type GeneralApiAccountResourceName = `general:${ keyof typeof GENERAL_API_ACCOUNT_RESOURCES }`;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import type React from 'react';
|
||||
|
||||
export const FallbackProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
return children;
|
||||
};
|
||||
@@ -73,7 +73,7 @@ Type extends EventTypes.LOGIN ? (
|
||||
Source: 'Email';
|
||||
} | {
|
||||
Action: 'Success';
|
||||
Source: 'Email' | 'Wallet';
|
||||
Source: 'Email' | 'Wallet' | 'Dynamic';
|
||||
}
|
||||
) :
|
||||
Type extends EventTypes.ACCOUNT_LINK_INFO ? {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { useDynamicContext, useUserWallets } from '@dynamic-labs/sdk-react-core';
|
||||
import React from 'react';
|
||||
import type { UseAccountReturnType } from 'wagmi';
|
||||
|
||||
export default function useAccountDynamic(): UseAccountReturnType {
|
||||
const userWallets = useUserWallets();
|
||||
const { primaryWallet } = useDynamicContext();
|
||||
|
||||
const address = (primaryWallet?.address || userWallets[0]?.address) as `0x${ string }` | undefined;
|
||||
|
||||
return React.useMemo(() => {
|
||||
if (!address) {
|
||||
return {
|
||||
address: undefined,
|
||||
addresses: undefined,
|
||||
chain: undefined,
|
||||
chainId: undefined,
|
||||
connector: undefined,
|
||||
isConnected: false,
|
||||
isConnecting: false,
|
||||
isDisconnected: true,
|
||||
isReconnecting: false,
|
||||
status: 'disconnected',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
address,
|
||||
addresses: [ address ],
|
||||
chain: undefined,
|
||||
chainId: undefined,
|
||||
connector: undefined,
|
||||
isConnected: true,
|
||||
isConnecting: false,
|
||||
isDisconnected: false,
|
||||
isReconnecting: false,
|
||||
status: 'connected',
|
||||
} as unknown as UseAccountReturnType;
|
||||
}, [ address ]);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import type { UseAccountReturnType } from 'wagmi';
|
||||
|
||||
export default function useAccountFallback(): UseAccountReturnType {
|
||||
return React.useMemo(() => ({
|
||||
address: undefined,
|
||||
addresses: undefined,
|
||||
chain: undefined,
|
||||
chainId: undefined,
|
||||
connector: undefined,
|
||||
isConnected: false,
|
||||
isConnecting: false,
|
||||
isDisconnected: true,
|
||||
isReconnecting: false,
|
||||
status: 'disconnected',
|
||||
}), []);
|
||||
}
|
||||
@@ -4,7 +4,11 @@ import appConfig from 'configs/app';
|
||||
import essentialDappsChainsConfig from 'configs/essential-dapps-chains';
|
||||
import multichainConfig from 'configs/multichain';
|
||||
|
||||
const getChainInfo = (config: Partial<typeof appConfig> = appConfig, contracts?: Chain['contracts']): Chain | undefined => {
|
||||
const getChainInfo = (
|
||||
config: Partial<typeof appConfig> = appConfig,
|
||||
contracts?: Chain['contracts'],
|
||||
logoUrl?: string,
|
||||
): Chain | undefined => {
|
||||
if (!config.chain || !config.app) {
|
||||
return;
|
||||
}
|
||||
@@ -30,6 +34,9 @@ const getChainInfo = (config: Partial<typeof appConfig> = appConfig, contracts?:
|
||||
},
|
||||
testnet: config.chain.isTestnet,
|
||||
contracts,
|
||||
custom: {
|
||||
logoUrl: logoUrl ?? config.UI?.navigation.icon.default,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -74,7 +81,7 @@ export const clusterChains: Array<Chain> | undefined = (() => {
|
||||
return;
|
||||
}
|
||||
|
||||
return config.chains.map(({ app_config: config }) => getChainInfo(config)).filter(Boolean);
|
||||
return config.chains.map(({ app_config: config, logo }) => getChainInfo(config, undefined, logo)).filter(Boolean);
|
||||
})();
|
||||
|
||||
export const essentialDappsChains: Array<Chain> | undefined = (() => {
|
||||
@@ -84,7 +91,7 @@ export const essentialDappsChains: Array<Chain> | undefined = (() => {
|
||||
return;
|
||||
}
|
||||
|
||||
return config.chains.map(({ app_config: config, contracts }) => getChainInfo(config, contracts)).filter(Boolean);
|
||||
return config.chains.map(({ app_config: config, contracts, logo }) => getChainInfo(config, contracts, logo)).filter(Boolean);
|
||||
})();
|
||||
|
||||
export const chains = (() => {
|
||||
|
||||
@@ -1,23 +1,12 @@
|
||||
import type { UseAccountReturnType } from 'wagmi';
|
||||
import { useAccount } from 'wagmi';
|
||||
|
||||
import config from 'configs/app';
|
||||
|
||||
function useAccountFallback(): UseAccountReturnType {
|
||||
return {
|
||||
address: undefined,
|
||||
addresses: undefined,
|
||||
chain: undefined,
|
||||
chainId: undefined,
|
||||
connector: undefined,
|
||||
isConnected: false,
|
||||
isConnecting: false,
|
||||
isDisconnected: true,
|
||||
isReconnecting: false,
|
||||
status: 'disconnected',
|
||||
};
|
||||
}
|
||||
const feature = config.features.blockchainInteraction;
|
||||
|
||||
const hook = config.features.blockchainInteraction.isEnabled ? useAccount : useAccountFallback;
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
const useAccount = (feature.isEnabled && feature.connectorType === 'dynamic') ?
|
||||
(await import('./account/useAccountDynamic')).default :
|
||||
(feature.isEnabled && feature.connectorType === 'reown') ?
|
||||
(await import('wagmi')).useAccount :
|
||||
(await import('./account/useAccountFallback')).default;
|
||||
|
||||
export default hook;
|
||||
export default useAccount;
|
||||
|
||||
@@ -1,63 +1,12 @@
|
||||
import { useAppKit, useAppKitState } from '@reown/appkit/react';
|
||||
import React from 'react';
|
||||
import { useDisconnect, useAccountEffect } from 'wagmi';
|
||||
import config from 'configs/app';
|
||||
|
||||
import * as mixpanel from 'lib/mixpanel/index';
|
||||
import useAccount from 'lib/web3/useAccount';
|
||||
const feature = config.features.blockchainInteraction;
|
||||
|
||||
interface Params {
|
||||
source: mixpanel.EventPayload<mixpanel.EventTypes.WALLET_CONNECT>['Source'];
|
||||
onConnect?: () => void;
|
||||
}
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
const useWallet = (feature.isEnabled && feature.connectorType === 'dynamic') ?
|
||||
(await import('./wallet/useWalletDynamic')).default :
|
||||
(feature.isEnabled && feature.connectorType === 'reown') ?
|
||||
(await import('./wallet/useWalletReown')).default :
|
||||
(await import('./wallet/useWalletFallback')).default;
|
||||
|
||||
export default function useWeb3Wallet({ source, onConnect }: Params) {
|
||||
const { open: openModal } = useAppKit();
|
||||
const { open: isOpen } = useAppKitState();
|
||||
const { disconnect } = useDisconnect();
|
||||
const [ isOpening, setIsOpening ] = React.useState(false);
|
||||
const [ isClientLoaded, setIsClientLoaded ] = React.useState(false);
|
||||
const isConnectionStarted = React.useRef(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
setIsClientLoaded(true);
|
||||
}, []);
|
||||
|
||||
const handleConnect = React.useCallback(async() => {
|
||||
setIsOpening(true);
|
||||
await openModal();
|
||||
setIsOpening(false);
|
||||
mixpanel.logEvent(mixpanel.EventTypes.WALLET_CONNECT, { Source: source, Status: 'Started' });
|
||||
isConnectionStarted.current = true;
|
||||
}, [ openModal, source ]);
|
||||
|
||||
const handleAccountConnected = React.useCallback(({ isReconnected }: { isReconnected: boolean }) => {
|
||||
if (!isReconnected && isConnectionStarted.current) {
|
||||
mixpanel.logEvent(mixpanel.EventTypes.WALLET_CONNECT, { Source: source, Status: 'Connected' });
|
||||
mixpanel.userProfile.setOnce({
|
||||
'With Connected Wallet': true,
|
||||
});
|
||||
onConnect?.();
|
||||
}
|
||||
isConnectionStarted.current = false;
|
||||
}, [ source, onConnect ]);
|
||||
|
||||
const handleDisconnect = React.useCallback(() => {
|
||||
disconnect();
|
||||
}, [ disconnect ]);
|
||||
|
||||
useAccountEffect({ onConnect: handleAccountConnected });
|
||||
|
||||
const account = useAccount();
|
||||
const address = account.address;
|
||||
const isConnected = isClientLoaded && !account.isDisconnected && account.address !== undefined;
|
||||
|
||||
return React.useMemo(() => ({
|
||||
connect: handleConnect,
|
||||
disconnect: handleDisconnect,
|
||||
isOpen: isOpening || isOpen,
|
||||
isConnected,
|
||||
isReconnecting: account.isReconnecting,
|
||||
address,
|
||||
openModal,
|
||||
}), [ handleConnect, handleDisconnect, isOpening, isOpen, isConnected, account.isReconnecting, address, openModal ]);
|
||||
}
|
||||
export default useWallet;
|
||||
|
||||
@@ -47,7 +47,7 @@ const reduceExternalChainsToTransportConfig = (readOnly: boolean): Record<string
|
||||
|
||||
const wagmi = (() => {
|
||||
|
||||
if (!feature.isEnabled) {
|
||||
if (!feature.isEnabled || feature.connectorType === 'dynamic') {
|
||||
const wagmiConfig = createConfig({
|
||||
chains: chains as [Chain, ...Array<Chain>],
|
||||
transports: {
|
||||
@@ -57,6 +57,7 @@ const wagmi = (() => {
|
||||
},
|
||||
ssr: true,
|
||||
batch: { multicall: { wait: 100, batchSize: 5 } },
|
||||
multiInjectedProviderDiscovery: feature.isEnabled && feature.connectorType === 'dynamic' ? false : true,
|
||||
});
|
||||
|
||||
return { config: wagmiConfig, adapter: null };
|
||||
@@ -70,7 +71,7 @@ const wagmi = (() => {
|
||||
...(parentChain ? { [parentChain.id]: http() } : {}),
|
||||
...reduceExternalChainsToTransportConfig(false),
|
||||
},
|
||||
projectId: feature.walletConnect.projectId,
|
||||
projectId: feature.reown.projectId,
|
||||
ssr: true,
|
||||
batch: { multicall: { wait: 100, batchSize: 5 } },
|
||||
syncConnectedChain: false,
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import type * as mixpanel from 'lib/mixpanel/index';
|
||||
|
||||
export interface Params {
|
||||
source: mixpanel.EventPayload<mixpanel.EventTypes.WALLET_CONNECT>['Source'];
|
||||
onConnect?: () => void;
|
||||
}
|
||||
|
||||
export interface Result {
|
||||
connect: () => void;
|
||||
disconnect: () => void;
|
||||
isOpen: boolean;
|
||||
isConnected: boolean;
|
||||
isReconnecting: boolean;
|
||||
address: string | undefined;
|
||||
openModal: () => void;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { useDynamicContext, useDynamicEvents, useUserWallets } from '@dynamic-labs/sdk-react-core';
|
||||
import React from 'react';
|
||||
import { useAccountEffect } from 'wagmi';
|
||||
|
||||
import type { Params, Result } from './types';
|
||||
|
||||
import * as mixpanel from 'lib/mixpanel/index';
|
||||
|
||||
import useAccountDynamic from '../account/useAccountDynamic';
|
||||
|
||||
export default function useWalletDynamic({ source, onConnect }: Params): Result {
|
||||
const isConnectionStarted = React.useRef(false);
|
||||
const [ isOpen ] = React.useState(false);
|
||||
const [ isClientLoaded, setIsClientLoaded ] = React.useState(false);
|
||||
|
||||
const { setShowDynamicUserProfile, setShowAuthFlow, setAuthMode, removeWallet } = useDynamicContext();
|
||||
|
||||
const openModal = React.useCallback(() => {
|
||||
setShowDynamicUserProfile(true);
|
||||
mixpanel.logEvent(mixpanel.EventTypes.ACCOUNT_ACCESS, { Action: 'Dropdown open' });
|
||||
}, [ setShowDynamicUserProfile ]);
|
||||
|
||||
useDynamicEvents('authFlowOpen', async() => {
|
||||
if (!isConnectionStarted.current) {
|
||||
mixpanel.logEvent(mixpanel.EventTypes.WALLET_CONNECT, { Source: source, Status: 'Started' });
|
||||
isConnectionStarted.current = true;
|
||||
}
|
||||
});
|
||||
|
||||
const handleAccountConnected = React.useCallback(({ isReconnected }: { isReconnected: boolean }) => {
|
||||
if (!isReconnected && isConnectionStarted.current) {
|
||||
mixpanel.logEvent(mixpanel.EventTypes.WALLET_CONNECT, { Source: source, Status: 'Connected' });
|
||||
mixpanel.userProfile.setOnce({
|
||||
'With Connected Wallet': true,
|
||||
});
|
||||
onConnect?.();
|
||||
}
|
||||
isConnectionStarted.current = false;
|
||||
}, [ source, onConnect ]);
|
||||
|
||||
const handleConnect = React.useCallback(() => {
|
||||
setAuthMode('connect-only');
|
||||
setShowAuthFlow(true);
|
||||
}, [ setAuthMode, setShowAuthFlow ]);
|
||||
|
||||
const userWallets = useUserWallets();
|
||||
const primaryWalletId = userWallets[0]?.id;
|
||||
|
||||
const handleDisconnect = React.useCallback(() => {
|
||||
primaryWalletId && removeWallet(primaryWalletId);
|
||||
}, [ primaryWalletId, removeWallet ]);
|
||||
|
||||
useAccountEffect({ onConnect: handleAccountConnected });
|
||||
|
||||
React.useEffect(() => {
|
||||
setIsClientLoaded(true);
|
||||
}, []);
|
||||
|
||||
const account = useAccountDynamic();
|
||||
const address = account.address;
|
||||
const isConnected = isClientLoaded && !account.isDisconnected && account.address !== undefined;
|
||||
|
||||
return React.useMemo(() => ({
|
||||
connect: handleConnect,
|
||||
disconnect: handleDisconnect,
|
||||
isOpen,
|
||||
isConnected,
|
||||
isReconnecting: false,
|
||||
address,
|
||||
openModal,
|
||||
}), [ handleConnect, handleDisconnect, isOpen, isConnected, address, openModal ]);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
|
||||
import type { Result } from './types';
|
||||
|
||||
export default function useWalletFallback(): Result {
|
||||
return React.useMemo(() => ({
|
||||
connect: () => {},
|
||||
disconnect: () => {},
|
||||
isOpen: false,
|
||||
isConnected: false,
|
||||
isReconnecting: false,
|
||||
address: undefined,
|
||||
openModal: () => {},
|
||||
}), []);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { useAppKit, useAppKitState } from '@reown/appkit/react';
|
||||
import React from 'react';
|
||||
import { useAccountEffect, useAccount, useDisconnect } from 'wagmi';
|
||||
|
||||
import type { Params, Result } from './types';
|
||||
|
||||
import * as mixpanel from 'lib/mixpanel/index';
|
||||
|
||||
export default function useWalletReown({ source, onConnect }: Params): Result {
|
||||
const { open: openModal } = useAppKit();
|
||||
const { open: isOpen } = useAppKitState();
|
||||
const { disconnect } = useDisconnect();
|
||||
const [ isOpening, setIsOpening ] = React.useState(false);
|
||||
const [ isClientLoaded, setIsClientLoaded ] = React.useState(false);
|
||||
const isConnectionStarted = React.useRef(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
setIsClientLoaded(true);
|
||||
}, []);
|
||||
|
||||
const handleConnect = React.useCallback(async() => {
|
||||
setIsOpening(true);
|
||||
await openModal();
|
||||
setIsOpening(false);
|
||||
mixpanel.logEvent(mixpanel.EventTypes.WALLET_CONNECT, { Source: source, Status: 'Started' });
|
||||
isConnectionStarted.current = true;
|
||||
}, [ openModal, source ]);
|
||||
|
||||
const handleAccountConnected = React.useCallback(({ isReconnected }: { isReconnected: boolean }) => {
|
||||
if (!isReconnected && isConnectionStarted.current) {
|
||||
mixpanel.logEvent(mixpanel.EventTypes.WALLET_CONNECT, { Source: source, Status: 'Connected' });
|
||||
mixpanel.userProfile.setOnce({
|
||||
'With Connected Wallet': true,
|
||||
});
|
||||
onConnect?.();
|
||||
}
|
||||
isConnectionStarted.current = false;
|
||||
}, [ source, onConnect ]);
|
||||
|
||||
const handleDisconnect = React.useCallback(() => {
|
||||
disconnect();
|
||||
}, [ disconnect ]);
|
||||
|
||||
useAccountEffect({ onConnect: handleAccountConnected });
|
||||
|
||||
const account = useAccount();
|
||||
const address = account.address;
|
||||
const isConnected = isClientLoaded && !account.isDisconnected && account.address !== undefined;
|
||||
|
||||
return React.useMemo(() => ({
|
||||
connect: handleConnect,
|
||||
disconnect: handleDisconnect,
|
||||
isOpen: isOpening || isOpen,
|
||||
isConnected,
|
||||
isReconnecting: account.isReconnecting,
|
||||
address,
|
||||
openModal,
|
||||
}), [ handleConnect, handleDisconnect, isOpening, isOpen, isConnected, account.isReconnecting, address, openModal ]);
|
||||
}
|
||||
@@ -25,6 +25,14 @@ const moduleExports = {
|
||||
);
|
||||
config.resolve.fallback = { fs: false, net: false, tls: false };
|
||||
config.externals.push('pino-pretty', 'lokijs', 'encoding');
|
||||
|
||||
config.experiments = { ...config.experiments, topLevelAwait: true };
|
||||
// Tell webpack the target supports async/await so it stops warning about top-level await
|
||||
// Top-level await is belong to ES2017 specification that is adopted by all major browsers and Node.js.
|
||||
config.output.environment = {
|
||||
...config.output.environment,
|
||||
asyncFunction: true,
|
||||
};
|
||||
|
||||
return config;
|
||||
},
|
||||
@@ -36,7 +44,7 @@ const moduleExports = {
|
||||
redirects,
|
||||
headers,
|
||||
output: 'standalone',
|
||||
productionBrowserSourceMaps: true,
|
||||
productionBrowserSourceMaps: false,
|
||||
serverExternalPackages: ["@opentelemetry/sdk-node", "@opentelemetry/auto-instrumentations-node"],
|
||||
experimental: {
|
||||
staleTimes: {
|
||||
|
||||
@@ -6,6 +6,7 @@ function generateCspPolicy(isPrivateMode = false) {
|
||||
descriptors.app(isPrivateMode),
|
||||
// Exclude tracking/analytics sources in private mode
|
||||
isPrivateMode ? {} : descriptors.ad(),
|
||||
isPrivateMode ? {} : descriptors.blockchainInteraction(),
|
||||
descriptors.cloudFlare(),
|
||||
descriptors.flashblocks(),
|
||||
descriptors.gasHawk(),
|
||||
@@ -23,7 +24,6 @@ function generateCspPolicy(isPrivateMode = false) {
|
||||
descriptors.rollup(),
|
||||
descriptors.safe(),
|
||||
descriptors.usernameApi(),
|
||||
isPrivateMode ? {} : descriptors.walletConnect(),
|
||||
descriptors.zetachain(),
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import type CspDev from 'csp-dev';
|
||||
|
||||
import config from 'configs/app';
|
||||
|
||||
import { KEY_WORDS } from '../utils';
|
||||
|
||||
const feature = config.features.blockchainInteraction;
|
||||
|
||||
export function blockchainInteraction(): CspDev.DirectiveDescriptor {
|
||||
if (!feature.isEnabled) {
|
||||
return {};
|
||||
}
|
||||
|
||||
switch (feature.connectorType) {
|
||||
case 'reown': {
|
||||
return {
|
||||
'connect-src': [
|
||||
'*.web3modal.com',
|
||||
'*.web3modal.org',
|
||||
'*.walletconnect.com',
|
||||
'*.walletconnect.org',
|
||||
'wss://relay.walletconnect.com',
|
||||
'wss://relay.walletconnect.org',
|
||||
'wss://www.walletlink.org',
|
||||
],
|
||||
'frame-ancestors': [
|
||||
'*.walletconnect.org',
|
||||
'*.walletconnect.com',
|
||||
],
|
||||
'img-src': [
|
||||
KEY_WORDS.BLOB,
|
||||
'*.walletconnect.com',
|
||||
],
|
||||
};
|
||||
}
|
||||
case 'dynamic': {
|
||||
return {
|
||||
'connect-src': [
|
||||
'https://dynamic-static-assets.com',
|
||||
'https://app.dynamicauth.com',
|
||||
'https://logs.dynamicauth.com',
|
||||
],
|
||||
'font-src': [
|
||||
'https://cdn.jsdelivr.net/npm/@fontsource/dm-sans/files/dm-sans-latin-400-normal.woff2',
|
||||
'https://cdn.jsdelivr.net/npm/@fontsource/dm-sans/files/dm-sans-latin-400-normal.woff',
|
||||
'https://cdn.jsdelivr.net/npm/@fontsource/dm-sans/files/dm-sans-latin-400-italic.woff2',
|
||||
'https://cdn.jsdelivr.net/npm/@fontsource/dm-sans/files/dm-sans-latin-400-italic.woff',
|
||||
'https://cdn.jsdelivr.net/npm/@fontsource/dm-sans/files/dm-sans-latin-500-normal.woff2',
|
||||
'https://cdn.jsdelivr.net/npm/@fontsource/dm-sans/files/dm-sans-latin-500-normal.woff',
|
||||
'https://cdn.jsdelivr.net/npm/@fontsource/dm-sans/files/dm-sans-latin-500-italic.woff2',
|
||||
'https://cdn.jsdelivr.net/npm/@fontsource/dm-sans/files/dm-sans-latin-500-italic.woff',
|
||||
'https://cdn.jsdelivr.net/npm/@fontsource/dm-sans/files/dm-sans-latin-700-normal.woff2',
|
||||
'https://cdn.jsdelivr.net/npm/@fontsource/dm-sans/files/dm-sans-latin-700-normal.woff',
|
||||
'https://cdn.jsdelivr.net/npm/@fontsource/dm-sans/files/dm-sans-latin-700-italic.woff2',
|
||||
'https://cdn.jsdelivr.net/npm/@fontsource/dm-sans/files/dm-sans-latin-700-italic.woff',
|
||||
],
|
||||
'style-src': [
|
||||
'https://app.dynamic.xyz',
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
export { ad } from './ad';
|
||||
export { app } from './app';
|
||||
export { blockchainInteraction } from './blockchainInteraction';
|
||||
export { cloudFlare } from './cloudFlare';
|
||||
export { flashblocks } from './flashblocks';
|
||||
export { gasHawk } from './gasHawk';
|
||||
@@ -17,5 +18,4 @@ export { rollbar } from './rollbar';
|
||||
export { rollup } from './rollup';
|
||||
export { safe } from './safe';
|
||||
export { usernameApi } from './usernameApi';
|
||||
export { walletConnect } from './walletConnect';
|
||||
export { zetachain } from './zetachain';
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import type CspDev from 'csp-dev';
|
||||
|
||||
import config from 'configs/app';
|
||||
|
||||
import { KEY_WORDS } from '../utils';
|
||||
|
||||
export function walletConnect(): CspDev.DirectiveDescriptor {
|
||||
if (!config.features.blockchainInteraction.isEnabled) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
'connect-src': [
|
||||
'*.web3modal.com',
|
||||
'*.web3modal.org',
|
||||
'*.walletconnect.com',
|
||||
'*.walletconnect.org',
|
||||
'wss://relay.walletconnect.com',
|
||||
'wss://relay.walletconnect.org',
|
||||
'wss://www.walletlink.org',
|
||||
],
|
||||
'frame-ancestors': [
|
||||
'*.walletconnect.org',
|
||||
'*.walletconnect.com',
|
||||
],
|
||||
'img-src': [
|
||||
KEY_WORDS.BLOB,
|
||||
'*.walletconnect.com',
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -18,6 +18,15 @@ export const account: Guard = (chainConfig: typeof config) => async() => {
|
||||
}
|
||||
};
|
||||
|
||||
export const accountAuth0: Guard = (chainConfig: typeof config) => async() => {
|
||||
const feature = chainConfig.features.account;
|
||||
if (!feature.isEnabled || feature.authProvider !== 'auth0') {
|
||||
return {
|
||||
notFound: true,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const verifiedAddresses: Guard = (chainConfig: typeof config) => async() => {
|
||||
if (!chainConfig.features.addressVerification.isEnabled) {
|
||||
return {
|
||||
|
||||
@@ -6,6 +6,7 @@ export const block = factory([ guards.notOpSuperchain ]);
|
||||
export const tx = factory([ guards.notOpSuperchain ]);
|
||||
export const token = factory([ guards.notOpSuperchain ]);
|
||||
export const account = factory([ guards.account ]);
|
||||
export const accountAuth0 = factory([ guards.accountAuth0 ]);
|
||||
export const verifiedAddresses = factory([ guards.account, guards.verifiedAddresses ]);
|
||||
export const userOps = factory([ guards.userOps ]);
|
||||
export const marketplace = factory([ guards.marketplace ]);
|
||||
|
||||
@@ -51,6 +51,9 @@
|
||||
"@blockscout/visualizer-types": "0.2.0",
|
||||
"@blockscout/zetachain-cctx-types": "^1.0.0-rc.6",
|
||||
"@chakra-ui/react": "3.15.0",
|
||||
"@dynamic-labs/ethereum": "4.52.2",
|
||||
"@dynamic-labs/sdk-react-core": "4.52.2",
|
||||
"@dynamic-labs/wagmi-connector": "4.52.2",
|
||||
"@emotion/react": "11.14.0",
|
||||
"@growthbook/growthbook-react": "0.21.0",
|
||||
"@helia/verified-fetch": "2.6.12",
|
||||
@@ -208,7 +211,9 @@
|
||||
"@reown/appkit-adapter-wagmi/**/node-forge": "1.3.2",
|
||||
"eslint/**/brace-expansion": "1.1.12",
|
||||
"swagger-ui-react/**/js-yaml": "4.1.1",
|
||||
"dappscout-iframe/**/ws": "8.17.1"
|
||||
"dappscout-iframe/**/ws": "8.17.1",
|
||||
"viem/**/@noble/hashes": "1.8.0",
|
||||
"@walletconnect/ethereum-provider/**/@reown/appkit/**/@noble/hashes": "1.8.0"
|
||||
},
|
||||
"packageManager": "[email protected]+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { GrowthBookProvider } from '@growthbook/growthbook-react';
|
||||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
import type { AppProps } from 'next/app';
|
||||
import dynamic from 'next/dynamic';
|
||||
import React from 'react';
|
||||
|
||||
import type { NextPageWithLayout } from 'nextjs/types';
|
||||
@@ -11,8 +12,8 @@ import config from 'configs/app';
|
||||
import getSocketUrl from 'lib/api/getSocketUrl';
|
||||
import useQueryClientConfig from 'lib/api/useQueryClientConfig';
|
||||
import { AppContextProvider } from 'lib/contexts/app';
|
||||
import { FallbackProvider } from 'lib/contexts/fallback';
|
||||
import { MarketplaceContextProvider } from 'lib/contexts/marketplace';
|
||||
import { RewardsContextProvider } from 'lib/contexts/rewards';
|
||||
import { SettingsContextProvider } from 'lib/contexts/settings';
|
||||
import { initGrowthBook } from 'lib/growthbook/init';
|
||||
import useLoadFeatures from 'lib/growthbook/useLoadFeatures';
|
||||
@@ -20,13 +21,15 @@ import { clientConfig as rollbarConfig, Provider as RollbarProvider } from 'lib/
|
||||
import { SocketProvider } from 'lib/socket/context';
|
||||
import { Provider as ChakraProvider } from 'toolkit/chakra/provider';
|
||||
import { Toaster } from 'toolkit/chakra/toaster';
|
||||
import RewardsLoginModal from 'ui/rewards/login/RewardsLoginModal';
|
||||
import RewardsActivityTracker from 'ui/rewards/RewardsActivityTracker';
|
||||
import AppErrorBoundary from 'ui/shared/AppError/AppErrorBoundary';
|
||||
import AppErrorGlobalContainer from 'ui/shared/AppError/AppErrorGlobalContainer';
|
||||
import GoogleAnalytics from 'ui/shared/GoogleAnalytics';
|
||||
import Layout from 'ui/shared/layout/Layout';
|
||||
import Web3ModalProvider from 'ui/shared/Web3ModalProvider';
|
||||
import Web3Provider from 'ui/shared/web3/Web3Provider';
|
||||
|
||||
const RewardsContextProvider = dynamic(() => import('lib/contexts/rewards').then(module => module.RewardsContextProvider), { ssr: false });
|
||||
const RewardsLoginModal = dynamic(() => import('ui/rewards/login/RewardsLoginModal'), { ssr: false });
|
||||
const RewardsActivityTracker = dynamic(() => import('ui/rewards/RewardsActivityTracker'), { ssr: false });
|
||||
|
||||
import 'lib/setLocale';
|
||||
// import 'focus-visible/dist/focus-visible';
|
||||
@@ -88,6 +91,8 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
||||
);
|
||||
})();
|
||||
|
||||
const RewardsProvider = config.features.rewards.isEnabled ? RewardsContextProvider : FallbackProvider;
|
||||
|
||||
const socketUrl = !config.features.opSuperchain.isEnabled ? getSocketUrl() : undefined;
|
||||
|
||||
return (
|
||||
@@ -97,25 +102,25 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
||||
{ ...ERROR_SCREEN_STYLES }
|
||||
Container={ AppErrorGlobalContainer }
|
||||
>
|
||||
<Web3ModalProvider>
|
||||
<AppContextProvider pageProps={ pageProps }>
|
||||
<QueryClientProvider client={ queryClient }>
|
||||
<QueryClientProvider client={ queryClient }>
|
||||
<Web3Provider>
|
||||
<AppContextProvider pageProps={ pageProps }>
|
||||
<GrowthBookProvider growthbook={ growthBook }>
|
||||
<SocketProvider url={ socketUrl }>
|
||||
<RewardsContextProvider>
|
||||
<RewardsProvider>
|
||||
<MarketplaceContextProvider>
|
||||
<SettingsContextProvider>
|
||||
{ content }
|
||||
</SettingsContextProvider>
|
||||
</MarketplaceContextProvider>
|
||||
</RewardsContextProvider>
|
||||
</RewardsProvider>
|
||||
</SocketProvider>
|
||||
</GrowthBookProvider>
|
||||
<ReactQueryDevtools buttonPosition="bottom-left" position="left"/>
|
||||
<GoogleAnalytics/>
|
||||
</QueryClientProvider>
|
||||
</AppContextProvider>
|
||||
</Web3ModalProvider>
|
||||
</AppContextProvider>
|
||||
</Web3Provider>
|
||||
</QueryClientProvider>
|
||||
</AppErrorBoundary>
|
||||
</RollbarProvider>
|
||||
</ChakraProvider>
|
||||
|
||||
@@ -15,4 +15,4 @@ const Page: NextPage = () => {
|
||||
|
||||
export default Page;
|
||||
|
||||
export { account as getServerSideProps } from 'nextjs/getServerSideProps/main';
|
||||
export { accountAuth0 as getServerSideProps } from 'nextjs/getServerSideProps/main';
|
||||
|
||||
@@ -68,6 +68,21 @@ const config: PlaywrightTestConfig = defineConfig({
|
||||
// https://github.com/storybookjs/builder-vite/issues/409#issuecomment-1152848986
|
||||
sourcemap: false,
|
||||
minify: false,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
// Ensure __envs exists in every chunk so code (e.g. next/router, configs/app) that
|
||||
// runs before index.ts/envs.js doesn't throw. Real envs are set when envs.js runs.
|
||||
//
|
||||
// Explanation:
|
||||
// With await import(...) in useAccount/useWallet, Vite turns those into separate chunks.
|
||||
// In Playwright CT, the component’s chunk (and its dependency graph, including useAccount → config → getEnvValue → __envs)
|
||||
// can load and run before the main entry that runs envs.js.
|
||||
// So when that chunk runs, window.__envs isn’t set yet and you get ReferenceError: __envs is not defined.
|
||||
//
|
||||
// https://rollupjs.org/configuration-options/#output-banner-output-footer
|
||||
banner: '(function(){if(typeof window!==\'undefined\')window.__envs=window.__envs||{};})();',
|
||||
},
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: [
|
||||
|
||||
@@ -78,7 +78,7 @@ echo "Affected flag: $affected_flag"
|
||||
echo "Files to run: $files_to_run"
|
||||
|
||||
dotenv \
|
||||
-v NODE_OPTIONS=\"--max-old-space-size=4096\" \
|
||||
-v NODE_OPTIONS=\"--max-old-space-size=8192\" \
|
||||
-e $config_file \
|
||||
-- playwright test -c playwright-ct.config.ts $files_to_run $args
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"target": "es2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export interface CsrfData {
|
||||
token: string;
|
||||
}
|
||||
|
||||
export type AuthProvider = 'auth0' | 'dynamic';
|
||||
|
||||
@@ -12,7 +12,7 @@ import { Skeleton } from 'toolkit/chakra/skeleton';
|
||||
import { useDisclosure } from 'toolkit/hooks/useDisclosure';
|
||||
import CustomAbiModal from 'ui/customAbi/CustomAbiModal/CustomAbiModal';
|
||||
import RawDataSnippet from 'ui/shared/RawDataSnippet';
|
||||
import AuthGuard from 'ui/snippets/auth/AuthGuard';
|
||||
import AuthGuard from 'ui/snippets/auth/guard/AuthGuard';
|
||||
import useIsAuth from 'ui/snippets/auth/useIsAuth';
|
||||
|
||||
import ContractAbi from './ContractAbi';
|
||||
|
||||
@@ -26,7 +26,7 @@ import { getFieldLabel, matchArray, transformFormDataToMethodArgs } from './util
|
||||
import type { ContractMethodFormFields } from './utils';
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
const NO_WALLET_CLIENT_TEXT = 'Blockchain interaction is not available at the moment since WalletConnect is not configured for this application. Please contact the service maintainer to make necessary changes in the service configuration.';
|
||||
const NO_WALLET_CLIENT_TEXT = 'Blockchain interaction is not available at the moment since a wallet connector is not configured for this application. Please contact the service maintainer to make necessary changes in the service configuration.';
|
||||
|
||||
interface Props {
|
||||
data: SmartContractMethod;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { getAddress, type Abi } from 'viem';
|
||||
import { useAccount, useWalletClient, useSwitchChain } from 'wagmi';
|
||||
import { encodeFunctionData, getAddress, type Abi } from 'viem';
|
||||
import { useAccount, useWalletClient, useSwitchChain, usePublicClient } from 'wagmi';
|
||||
|
||||
import type { FormSubmitResult, SmartContractMethod } from './types';
|
||||
|
||||
@@ -10,6 +10,8 @@ import useRewardsActivity from 'lib/hooks/useRewardsActivity';
|
||||
|
||||
import { getNativeCoinValue } from './utils';
|
||||
|
||||
const feature = config.features.blockchainInteraction;
|
||||
|
||||
interface Params {
|
||||
item: SmartContractMethod;
|
||||
args: Array<unknown>;
|
||||
@@ -19,8 +21,10 @@ interface Params {
|
||||
export default function useCallMethodWalletClient(): (params: Params) => Promise<FormSubmitResult> {
|
||||
const multichainContext = useMultichainContext();
|
||||
const chainConfig = (multichainContext?.chain.app_config ?? config).chain;
|
||||
const targetChainId = chainConfig?.id ? Number(chainConfig.id) : undefined;
|
||||
|
||||
const { data: walletClient } = useWalletClient({ chainId: Number(chainConfig?.id) });
|
||||
const { data: walletClient } = useWalletClient({ chainId: targetChainId });
|
||||
const publicClient = usePublicClient({ chainId: targetChainId });
|
||||
const { isConnected, chainId, address: account } = useAccount();
|
||||
const { switchChainAsync } = useSwitchChain();
|
||||
const { trackTransaction, trackTransactionConfirm } = useRewardsActivity();
|
||||
@@ -34,8 +38,8 @@ export default function useCallMethodWalletClient(): (params: Params) => Promise
|
||||
throw new Error('Wallet Client is not defined');
|
||||
}
|
||||
|
||||
if (chainId && String(chainId) !== chainConfig?.id) {
|
||||
await switchChainAsync({ chainId: Number(chainConfig?.id) });
|
||||
if (chainId && targetChainId && chainId !== targetChainId) {
|
||||
await switchChainAsync({ chainId: targetChainId });
|
||||
}
|
||||
|
||||
const address = getAddress(addressHash);
|
||||
@@ -50,9 +54,20 @@ export default function useCallMethodWalletClient(): (params: Params) => Promise
|
||||
// if the fallback method acts as a read method, it can only have one input of type bytes
|
||||
// so we pass the input value as data without encoding it
|
||||
const data = typeof _args[0] === 'string' && _args[0].startsWith('0x') ? _args[0] as `0x${ string }` : undefined;
|
||||
|
||||
// seems like Dynamic WaaS (assigned when user signed up with email) does not estimate gas for transactions
|
||||
// so we have to do it manually
|
||||
const estimatedGas = feature.isEnabled && feature.connectorType === 'dynamic' ? await publicClient?.estimateGas({
|
||||
account,
|
||||
to: address,
|
||||
value,
|
||||
data: item.type === 'fallback' ? data : undefined,
|
||||
}) : undefined;
|
||||
|
||||
const hash = await walletClient.sendTransaction({
|
||||
to: address,
|
||||
value,
|
||||
gas: estimatedGas,
|
||||
...(data ? { data } : {}),
|
||||
});
|
||||
|
||||
@@ -69,6 +84,19 @@ export default function useCallMethodWalletClient(): (params: Params) => Promise
|
||||
throw new Error('Method name is not defined');
|
||||
}
|
||||
|
||||
// seems like Dynamic WaaS (assigned when user signed up with email) does not estimate gas for transactions
|
||||
// so we have to do it manually
|
||||
const estimatedGas = feature.isEnabled && feature.connectorType === 'dynamic' ? await publicClient?.estimateGas({
|
||||
account,
|
||||
to: address,
|
||||
value,
|
||||
data: encodeFunctionData({
|
||||
abi: [ item ],
|
||||
functionName: methodName,
|
||||
args: _args,
|
||||
}),
|
||||
}) : undefined;
|
||||
|
||||
const hash = await walletClient.writeContract({
|
||||
args: _args,
|
||||
// Here we provide the ABI as an array containing only one item from the submitted form.
|
||||
@@ -83,6 +111,7 @@ export default function useCallMethodWalletClient(): (params: Params) => Promise
|
||||
address,
|
||||
value,
|
||||
account,
|
||||
gas: estimatedGas,
|
||||
});
|
||||
|
||||
if (activityResponse?.token) {
|
||||
@@ -90,5 +119,5 @@ export default function useCallMethodWalletClient(): (params: Params) => Promise
|
||||
}
|
||||
|
||||
return { source: 'wallet_client', data: { hash } };
|
||||
}, [ chainId, chainConfig, isConnected, switchChainAsync, walletClient, account, trackTransaction, trackTransactionConfirm ]);
|
||||
}, [ isConnected, walletClient, chainId, targetChainId, trackTransaction, account, publicClient, switchChainAsync, trackTransactionConfirm ]);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { IconButton } from 'toolkit/chakra/icon-button';
|
||||
import { Tooltip } from 'toolkit/chakra/tooltip';
|
||||
import { useDisclosure } from 'toolkit/hooks/useDisclosure';
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
import AuthGuard from 'ui/snippets/auth/AuthGuard';
|
||||
import AuthGuard from 'ui/snippets/auth/guard/AuthGuard';
|
||||
import useProfileQuery from 'ui/snippets/auth/useProfileQuery';
|
||||
import WatchlistAddModal from 'ui/watchlist/AddressModal/AddressModal';
|
||||
import DeleteAddressModal from 'ui/watchlist/DeleteAddressModal';
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import dynamic from 'next/dynamic';
|
||||
import React from 'react';
|
||||
|
||||
import type { AddressVerificationFormFirstStepFields, AddressCheckStatusSuccess } from './types';
|
||||
import type { VerifiedAddress } from 'types/api/account';
|
||||
|
||||
import config from 'configs/app';
|
||||
import { FallbackProvider } from 'lib/contexts/fallback';
|
||||
import * as mixpanel from 'lib/mixpanel/index';
|
||||
import { DialogBody, DialogContent, DialogHeader, DialogRoot } from 'toolkit/chakra/dialog';
|
||||
import Web3ModalProvider from 'ui/shared/Web3ModalProvider';
|
||||
|
||||
const Web3ProviderBase = dynamic(() => import('ui/shared/web3/Web3Provider'), { ssr: false });
|
||||
|
||||
import AddressVerificationStepAddress from './steps/AddressVerificationStepAddress';
|
||||
import AddressVerificationStepSignature from './steps/AddressVerificationStepSignature';
|
||||
@@ -14,6 +17,10 @@ import AddressVerificationStepSuccess from './steps/AddressVerificationStepSucce
|
||||
|
||||
type StateData = AddressVerificationFormFirstStepFields & AddressCheckStatusSuccess & { isToken?: boolean };
|
||||
|
||||
const feature = config.features.blockchainInteraction;
|
||||
// Dynamic providers cannot be nested, so a dummy provider is used here
|
||||
const Web3Provider = feature.isEnabled && feature.connectorType === 'dynamic' ? FallbackProvider : Web3ProviderBase;
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onOpenChange: ({ open }: { open: boolean }) => void;
|
||||
@@ -115,9 +122,9 @@ const AddressVerificationModal = ({ defaultAddress, open, onOpenChange, onSubmit
|
||||
{ step.title }
|
||||
</DialogHeader>
|
||||
<DialogBody mb={ 0 }>
|
||||
<Web3ModalProvider>
|
||||
<Web3Provider>
|
||||
{ step.content }
|
||||
</Web3ModalProvider>
|
||||
</Web3Provider>
|
||||
</DialogBody>
|
||||
</DialogContent>
|
||||
</DialogRoot>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Box, chakra, Flex } from '@chakra-ui/react';
|
||||
import { useAppKit } from '@reown/appkit/react';
|
||||
import React from 'react';
|
||||
import type { SubmitHandler } from 'react-hook-form';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
@@ -18,6 +17,7 @@ import type { VerifiedAddress } from 'types/api/account';
|
||||
import config from 'configs/app';
|
||||
import useApiFetch from 'lib/api/useApiFetch';
|
||||
import shortenString from 'lib/shortenString';
|
||||
import useWallet from 'lib/web3/useWallet';
|
||||
import { Alert } from 'toolkit/chakra/alert';
|
||||
import { Button } from 'toolkit/chakra/button';
|
||||
import { Link } from 'toolkit/chakra/link';
|
||||
@@ -39,8 +39,8 @@ interface Props extends AddressVerificationFormFirstStepFields, AddressCheckStat
|
||||
const AddressVerificationStepSignature = ({ address, signingMessage, contractCreator, contractOwner, onContinue, noWeb3Provider }: Props) => {
|
||||
const [ signMethod, setSignMethod ] = React.useState<SignMethod>(noWeb3Provider ? 'manual' : 'wallet');
|
||||
|
||||
const { open: openWeb3Modal } = useAppKit();
|
||||
const { isConnected } = useAccount();
|
||||
const { openModal: openWeb3Modal } = useWallet({ source: 'Smart contracts' });
|
||||
|
||||
const formApi = useForm<Fields>({
|
||||
mode: 'onBlur',
|
||||
|
||||
@@ -78,7 +78,7 @@ const GraphQL = () => {
|
||||
url: graphqlUrl,
|
||||
// graphql ws implementation with absinthe plugin is incompatible with graphiql-ws protocol
|
||||
// or the older one subscriptions-transport-ws
|
||||
// so we (isstuev & vbaranov) decided to configure playground without subscriptions
|
||||
// so we (@isstuev & @vbaranov) decided to configure playground without subscriptions
|
||||
// in case of any complaint consider reconfigure the graphql ws server with absinthe_graphql_ws package
|
||||
// subscriptionUrl: `wss://${config.app.host}/socket/`,
|
||||
});
|
||||
|
||||
@@ -9,8 +9,7 @@ import RewardsButton from 'ui/rewards/RewardsButton';
|
||||
import AdBanner from 'ui/shared/ad/AdBanner';
|
||||
import SearchBar from 'ui/snippets/searchBar/SearchBarDesktop';
|
||||
import SearchBarMobile from 'ui/snippets/searchBar/SearchBarMobile';
|
||||
import UserProfileDesktop from 'ui/snippets/user/profile/UserProfileDesktop';
|
||||
import UserWalletDesktop from 'ui/snippets/user/wallet/UserWalletDesktop';
|
||||
import UserProfileDesktop from 'ui/snippets/user/UserProfileDesktop';
|
||||
|
||||
export const BACKGROUND_DEFAULT =
|
||||
'radial-gradient(103.03% 103.03% at 0% 0%, rgba(183, 148, 244, 0.8) 0%, rgba(0, 163, 196, 0.8) 100%), var(--chakra-colors-blue-400)';
|
||||
@@ -78,10 +77,7 @@ const HeroBanner = () => {
|
||||
{ config.UI.navigation.layout === 'vertical' && (
|
||||
<Box display={{ base: 'none', lg: 'flex' }} gap={ 2 }>
|
||||
{ config.features.rewards.isEnabled && <RewardsButton variant="hero"/> }
|
||||
{
|
||||
(config.features.account.isEnabled && <UserProfileDesktop buttonVariant="hero"/>) ||
|
||||
(config.features.blockchainInteraction.isEnabled && <UserWalletDesktop buttonVariant="hero"/>)
|
||||
}
|
||||
<UserProfileDesktop buttonVariant="hero"/>
|
||||
</Box>
|
||||
) }
|
||||
</Flex>
|
||||
|
||||
@@ -14,8 +14,7 @@ import { BackToButton } from 'toolkit/components/buttons/BackToButton';
|
||||
import { makePrettyLink } from 'toolkit/utils/url';
|
||||
import RewardsButton from 'ui/rewards/RewardsButton';
|
||||
import NetworkIcon from 'ui/snippets/networkLogo/NetworkIcon';
|
||||
import UserProfileDesktop from 'ui/snippets/user/profile/UserProfileDesktop';
|
||||
import UserWalletDesktop from 'ui/snippets/user/wallet/UserWalletDesktop';
|
||||
import UserProfileDesktop from 'ui/snippets/user/UserProfileDesktop';
|
||||
|
||||
import MarketplaceAppInfo from './MarketplaceAppInfo';
|
||||
import Rating from './Rating/Rating';
|
||||
@@ -76,10 +75,7 @@ const MarketplaceAppTopBar = ({ appId, data, isLoading }: Props) => {
|
||||
{ !isMobile && (
|
||||
<Flex ml="auto" gap={ 2 }>
|
||||
{ config.features.rewards.isEnabled && <RewardsButton size="sm"/> }
|
||||
{
|
||||
(config.features.account.isEnabled && <UserProfileDesktop buttonSize="sm"/>) ||
|
||||
(config.features.blockchainInteraction.isEnabled && <UserWalletDesktop buttonSize="sm"/>)
|
||||
}
|
||||
<UserProfileDesktop buttonSize="sm"/>
|
||||
</Flex>
|
||||
) }
|
||||
</Flex>
|
||||
|
||||
@@ -13,7 +13,7 @@ import { useDisclosure } from 'toolkit/hooks/useDisclosure';
|
||||
import AddressModal from 'ui/privateTags/AddressModal/AddressModal';
|
||||
import TransactionModal from 'ui/privateTags/TransactionModal/TransactionModal';
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
import AuthGuard from 'ui/snippets/auth/AuthGuard';
|
||||
import AuthGuard from 'ui/snippets/auth/guard/AuthGuard';
|
||||
|
||||
import ButtonItem from '../parts/ButtonItem';
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import { MenuItem } from 'toolkit/chakra/menu';
|
||||
import { useDisclosure } from 'toolkit/hooks/useDisclosure';
|
||||
import AddressVerificationModal from 'ui/addressVerification/AddressVerificationModal';
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
import AuthGuard from 'ui/snippets/auth/AuthGuard';
|
||||
import AuthGuard from 'ui/snippets/auth/guard/AuthGuard';
|
||||
import useIsAuth from 'ui/snippets/auth/useIsAuth';
|
||||
|
||||
import ButtonItem from '../parts/ButtonItem';
|
||||
|
||||
@@ -3,7 +3,6 @@ import React from 'react';
|
||||
|
||||
import { route } from 'nextjs-routes';
|
||||
|
||||
import config from 'configs/app';
|
||||
import getErrorCause from 'lib/errors/getErrorCause';
|
||||
import getErrorCauseStatusCode from 'lib/errors/getErrorCauseStatusCode';
|
||||
import getErrorObjStatusCode from 'lib/errors/getErrorObjStatusCode';
|
||||
@@ -11,16 +10,14 @@ import getErrorProp from 'lib/errors/getErrorProp';
|
||||
import getResourceErrorPayload from 'lib/errors/getResourceErrorPayload';
|
||||
import { Button } from 'toolkit/chakra/button';
|
||||
import { Link } from 'toolkit/chakra/link';
|
||||
import AdBannerContent from 'ui/shared/ad/AdBannerContent';
|
||||
|
||||
import AdBanner from '../ad/AdBanner';
|
||||
import AppErrorIcon from './AppErrorIcon';
|
||||
import AppErrorTitle from './AppErrorTitle';
|
||||
import AppErrorBlockConsensus from './custom/AppErrorBlockConsensus';
|
||||
import AppErrorTooManyRequests from './custom/AppErrorTooManyRequests';
|
||||
import AppErrorTxNotFound from './custom/AppErrorTxNotFound';
|
||||
|
||||
const adBannerConfig = config.features.adsBanner;
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
error: Error | undefined;
|
||||
@@ -91,8 +88,6 @@ const AppError = ({ error, className }: Props) => {
|
||||
default: {
|
||||
const { title, text } = ERROR_TEXTS[String(statusCode)] ?? ERROR_TEXTS[500];
|
||||
|
||||
const adBannerProvider = adBannerConfig.isEnabled ? adBannerConfig.provider : null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppErrorIcon statusCode={ statusCode }/>
|
||||
@@ -109,7 +104,7 @@ const AppError = ({ error, className }: Props) => {
|
||||
Back to home
|
||||
</Button>
|
||||
</Link>
|
||||
{ statusCode === 404 && adBannerProvider && <AdBannerContent mt={ 12 } provider={ adBannerProvider }/> }
|
||||
{ statusCode === 404 && <AdBanner mt={ 12 }/> }
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
import config from 'configs/app';
|
||||
|
||||
const ReownProvider = dynamic(() => import('./providers/ReownProvider'), { ssr: false });
|
||||
const DynamicProvider = dynamic(() => import('./providers/DynamicProvider'), { ssr: false });
|
||||
const WagmiProvider = dynamic(() => import('./providers/WagmiProvider'), { ssr: false });
|
||||
|
||||
const feature = config.features.blockchainInteraction;
|
||||
|
||||
const Web3Provider = (() => {
|
||||
if (feature.isEnabled && feature.connectorType === 'reown') {
|
||||
return ReownProvider;
|
||||
}
|
||||
|
||||
if (feature.isEnabled && feature.connectorType === 'dynamic') {
|
||||
return DynamicProvider;
|
||||
}
|
||||
|
||||
return WagmiProvider;
|
||||
})();
|
||||
|
||||
export default Web3Provider;
|
||||
@@ -0,0 +1,115 @@
|
||||
import { EthereumWalletConnectors } from '@dynamic-labs/ethereum';
|
||||
import type { DynamicContextProps, OnAuthSuccess } from '@dynamic-labs/sdk-react-core';
|
||||
import { DynamicContextProvider, getAuthToken } from '@dynamic-labs/sdk-react-core';
|
||||
import { DynamicWagmiConnector } from '@dynamic-labs/wagmi-connector';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import React from 'react';
|
||||
|
||||
import type { UserInfo } from 'types/api/account';
|
||||
|
||||
import config from 'configs/app';
|
||||
import useApiFetch from 'lib/api/useApiFetch';
|
||||
import { getResourceKey } from 'lib/api/useApiQuery';
|
||||
import getErrorMessage from 'lib/errors/getErrorMessage';
|
||||
import useGetCsrfToken from 'lib/hooks/useGetCsrfToken';
|
||||
import * as mixpanel from 'lib/mixpanel/index';
|
||||
import { chains } from 'lib/web3/chains';
|
||||
import { toaster } from 'toolkit/chakra/toaster';
|
||||
import { castToString } from 'toolkit/utils/guards';
|
||||
import useLogout from 'ui/snippets/auth/useLogout';
|
||||
|
||||
import WagmiProvider from './WagmiProvider';
|
||||
|
||||
const feature = config.features.blockchainInteraction;
|
||||
|
||||
const walletConnectors = [
|
||||
EthereumWalletConnectors,
|
||||
];
|
||||
|
||||
const overrides: DynamicContextProps['settings']['overrides'] = {
|
||||
evmNetworks: chains.map((chain) => {
|
||||
const logoUrl = castToString(chain.custom?.logoUrl);
|
||||
return {
|
||||
chainId: chain.id,
|
||||
name: chain.name,
|
||||
rpcUrls: chain.rpcUrls.default.http.slice(),
|
||||
nativeCurrency: chain.nativeCurrency,
|
||||
blockExplorerUrls: chain.blockExplorers ? [ chain.blockExplorers.default.url ] : [],
|
||||
networkId: chain.id,
|
||||
iconUrls: logoUrl ? [ logoUrl ] : [],
|
||||
isTestnet: chain.testnet,
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const DynamicProvider = ({ children }: Props) => {
|
||||
|
||||
const apiFetch = useApiFetch();
|
||||
const queryClient = useQueryClient();
|
||||
const csrfQuery = useGetCsrfToken();
|
||||
const onLogout = useLogout();
|
||||
|
||||
const onAuthSuccess: OnAuthSuccess = React.useCallback(async() => {
|
||||
try {
|
||||
const authToken = getAuthToken();
|
||||
const response = await apiFetch<'general:auth_dynamic', UserInfo, unknown>('general:auth_dynamic', {
|
||||
fetchParams: {
|
||||
headers: {
|
||||
Authorization: `Bearer ${ authToken }`,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!('name' in response)) {
|
||||
throw Error('Something went wrong');
|
||||
}
|
||||
queryClient.setQueryData(getResourceKey('general:user_info'), () => response);
|
||||
csrfQuery.refetch();
|
||||
mixpanel.logEvent(mixpanel.EventTypes.LOGIN, {
|
||||
Action: 'Success',
|
||||
Source: 'Dynamic',
|
||||
});
|
||||
} catch (error) {
|
||||
toaster.error({
|
||||
title: 'Error',
|
||||
description: getErrorMessage(error),
|
||||
});
|
||||
}
|
||||
}, [ apiFetch, csrfQuery, queryClient ]);
|
||||
|
||||
const settings: DynamicContextProps['settings'] = React.useMemo(() => {
|
||||
const environmentId = feature.isEnabled && feature.connectorType === 'dynamic' ? feature.dynamic.environmentId : undefined;
|
||||
|
||||
if (!environmentId) {
|
||||
throw new Error('Dynamic environment ID is not set');
|
||||
}
|
||||
|
||||
return {
|
||||
walletConnectors,
|
||||
environmentId,
|
||||
useMetamaskSdk: false,
|
||||
overrides,
|
||||
events: {
|
||||
onAuthSuccess,
|
||||
onLogout,
|
||||
},
|
||||
};
|
||||
}, [ onAuthSuccess, onLogout ]);
|
||||
|
||||
return (
|
||||
<DynamicContextProvider
|
||||
settings={ settings }
|
||||
>
|
||||
<WagmiProvider>
|
||||
<DynamicWagmiConnector>
|
||||
{ children }
|
||||
</DynamicWagmiConnector>
|
||||
</WagmiProvider>
|
||||
</DynamicContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(DynamicProvider);
|
||||
@@ -1,8 +1,6 @@
|
||||
/* eslint-disable consistent-default-export-name/default-export-match-filename */
|
||||
import type { AppKitNetwork } from '@reown/appkit/networks';
|
||||
import { createAppKit, useAppKitTheme } from '@reown/appkit/react';
|
||||
import React from 'react';
|
||||
import { WagmiProvider } from 'wagmi';
|
||||
|
||||
import config from 'configs/app';
|
||||
import { chains } from 'lib/web3/chains';
|
||||
@@ -12,11 +10,13 @@ import colors from 'toolkit/theme/foundations/colors';
|
||||
import { BODY_TYPEFACE } from 'toolkit/theme/foundations/typography';
|
||||
import zIndex from 'toolkit/theme/foundations/zIndex';
|
||||
|
||||
import WagmiProvider from './WagmiProvider';
|
||||
|
||||
const feature = config.features.blockchainInteraction;
|
||||
|
||||
const init = () => {
|
||||
const initReown = () => {
|
||||
try {
|
||||
if (!feature.isEnabled || !wagmiConfig.adapter) {
|
||||
if (!feature.isEnabled || !wagmiConfig.adapter || feature.connectorType === 'dynamic') {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ const init = () => {
|
||||
url: config.app.baseUrl,
|
||||
icons: [ config.UI.navigation.icon.default ].filter(Boolean),
|
||||
},
|
||||
projectId: feature.walletConnect.projectId,
|
||||
projectId: feature.reown.projectId,
|
||||
features: {
|
||||
analytics: false,
|
||||
email: false,
|
||||
@@ -43,27 +43,19 @@ const init = () => {
|
||||
'--w3m-border-radius-master': '2px',
|
||||
'--w3m-z-index': zIndex?.modal2?.value,
|
||||
},
|
||||
featuredWalletIds: feature.walletConnect.featuredWalletIds,
|
||||
featuredWalletIds: feature.reown.featuredWalletIds,
|
||||
allowUnsupportedChain: true,
|
||||
});
|
||||
} catch (error) {}
|
||||
};
|
||||
|
||||
init();
|
||||
initReown();
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const DefaultProvider = ({ children }: Props) => {
|
||||
return (
|
||||
<WagmiProvider config={ wagmiConfig.config }>
|
||||
{ children }
|
||||
</WagmiProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const Web3ModalProvider = ({ children }: Props) => {
|
||||
const ReownProvider = ({ children }: Props) => {
|
||||
const { colorMode } = useColorMode();
|
||||
const { setThemeMode } = useAppKitTheme();
|
||||
|
||||
@@ -72,12 +64,10 @@ const Web3ModalProvider = ({ children }: Props) => {
|
||||
}, [ colorMode, setThemeMode ]);
|
||||
|
||||
return (
|
||||
<DefaultProvider>
|
||||
<WagmiProvider>
|
||||
{ children }
|
||||
</DefaultProvider>
|
||||
</WagmiProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const Provider = feature.isEnabled ? Web3ModalProvider : DefaultProvider;
|
||||
|
||||
export default Provider;
|
||||
export default React.memo(ReownProvider);
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import { WagmiProvider as WagmiProviderCore } from 'wagmi';
|
||||
|
||||
import wagmiConfig from 'lib/web3/wagmiConfig';
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const WagmiProvider = ({ children }: Props) => {
|
||||
return (
|
||||
<WagmiProviderCore config={ wagmiConfig.config }>
|
||||
{ children }
|
||||
</WagmiProviderCore>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(WagmiProvider);
|
||||
@@ -0,0 +1,18 @@
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
import config from 'configs/app';
|
||||
|
||||
const AuthGuardAuth0 = dynamic(() => import('./AuthGuardAuth0'), { ssr: false });
|
||||
const AuthGuardDynamic = dynamic(() => import('./AuthGuardDynamic'), { ssr: false });
|
||||
|
||||
const feature = config.features.account;
|
||||
|
||||
const AuthGuard = (() => {
|
||||
if (feature.isEnabled && feature.authProvider === 'dynamic') {
|
||||
return AuthGuardDynamic;
|
||||
}
|
||||
|
||||
return AuthGuardAuth0;
|
||||
})();
|
||||
|
||||
export default AuthGuard;
|
||||
@@ -2,8 +2,8 @@ import React from 'react';
|
||||
|
||||
import { useDisclosure } from 'toolkit/hooks/useDisclosure';
|
||||
|
||||
import AuthModal from './AuthModal';
|
||||
import useProfileQuery from './useProfileQuery';
|
||||
import AuthModal from '../AuthModal';
|
||||
import useProfileQuery from '../useProfileQuery';
|
||||
|
||||
interface InjectedProps {
|
||||
onClick: () => void;
|
||||
@@ -15,7 +15,7 @@ interface Props {
|
||||
ensureEmail?: boolean;
|
||||
}
|
||||
|
||||
const AuthGuard = ({ children, onAuthSuccess, ensureEmail }: Props) => {
|
||||
const AuthGuardAuth0 = ({ children, onAuthSuccess, ensureEmail }: Props) => {
|
||||
const authModal = useDisclosure();
|
||||
const profileQuery = useProfileQuery();
|
||||
|
||||
@@ -61,4 +61,4 @@ const AuthGuard = ({ children, onAuthSuccess, ensureEmail }: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(AuthGuard);
|
||||
export default React.memo(AuthGuardAuth0);
|
||||
@@ -0,0 +1,64 @@
|
||||
import { useDynamicContext, useDynamicEvents } from '@dynamic-labs/sdk-react-core';
|
||||
import React from 'react';
|
||||
|
||||
import useLinkEmailDynamic from '../linkEmail/useLinkEmailDynamic';
|
||||
import useProfileQuery from '../useProfileQuery';
|
||||
|
||||
interface InjectedProps {
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
children: (props: InjectedProps) => React.ReactNode;
|
||||
onAuthSuccess: () => void;
|
||||
ensureEmail?: boolean;
|
||||
}
|
||||
|
||||
const AuthGuardDynamic = ({ children, onAuthSuccess, ensureEmail }: Props) => {
|
||||
const isAuthStartedRef = React.useRef(false);
|
||||
const profileQuery = useProfileQuery();
|
||||
const { setShowAuthFlow } = useDynamicContext();
|
||||
const { user } = useDynamicContext();
|
||||
|
||||
const linkEmailToProfile = useLinkEmailDynamic();
|
||||
|
||||
const handleClick = React.useCallback(() => {
|
||||
if (profileQuery.data) {
|
||||
if (ensureEmail && !profileQuery.data?.email) {
|
||||
linkEmailToProfile();
|
||||
} else {
|
||||
onAuthSuccess();
|
||||
}
|
||||
} else {
|
||||
setShowAuthFlow(true);
|
||||
isAuthStartedRef.current = true;
|
||||
}
|
||||
}, [ profileQuery.data, ensureEmail, linkEmailToProfile, onAuthSuccess, setShowAuthFlow ]);
|
||||
|
||||
const onAuthFlowEnd = React.useCallback(() => {
|
||||
if (!user || !isAuthStartedRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
isAuthStartedRef.current = false;
|
||||
if (ensureEmail && !user?.email) {
|
||||
linkEmailToProfile();
|
||||
} else {
|
||||
onAuthSuccess();
|
||||
}
|
||||
}, [ ensureEmail, linkEmailToProfile, onAuthSuccess, user ]);
|
||||
|
||||
// This event is triggered when user is logged in with wallet
|
||||
useDynamicEvents('authFlowClose', async() => {
|
||||
onAuthFlowEnd();
|
||||
});
|
||||
|
||||
// This event is triggered when user is logged in with email
|
||||
useDynamicEvents('emailVerificationResult', () => {
|
||||
onAuthFlowEnd();
|
||||
});
|
||||
|
||||
return children({ onClick: handleClick });
|
||||
};
|
||||
|
||||
export default React.memo(AuthGuardDynamic);
|
||||
@@ -0,0 +1,9 @@
|
||||
import config from 'configs/app';
|
||||
|
||||
const feature = config.features.account;
|
||||
|
||||
const useLinkEmail = (feature.isEnabled && feature.authProvider === 'dynamic') ?
|
||||
(await import('./useLinkEmailDynamic')).default :
|
||||
(await import('./useLinkEmailFallback')).default;
|
||||
|
||||
export default useLinkEmail;
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useUserUpdateRequest } from '@dynamic-labs/sdk-react-core';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import React from 'react';
|
||||
|
||||
import type { UserInfo } from 'types/api/account';
|
||||
|
||||
import { getResourceKey } from 'lib/api/useApiQuery';
|
||||
import * as mixpanel from 'lib/mixpanel/index';
|
||||
|
||||
export default function useLinkEmailDynamic(): () => void {
|
||||
const { updateUserWithModal } = useUserUpdateRequest();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return React.useCallback(() => {
|
||||
mixpanel.logEvent(mixpanel.EventTypes.ACCOUNT_LINK_INFO, {
|
||||
Status: 'Started',
|
||||
Type: 'Email',
|
||||
Source: 'Profile dropdown',
|
||||
});
|
||||
updateUserWithModal([ 'email' ]).then((fields) => {
|
||||
queryClient.setQueryData(getResourceKey('general:user_info'), (prevData: UserInfo | undefined) => {
|
||||
return { ...prevData, email: fields.email };
|
||||
});
|
||||
mixpanel.logEvent(mixpanel.EventTypes.ACCOUNT_LINK_INFO, {
|
||||
Status: 'Finished',
|
||||
Type: 'Email',
|
||||
Source: 'Profile dropdown',
|
||||
});
|
||||
});
|
||||
}, [ queryClient, updateUserWithModal ]);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function useLinkEmailFallback() {
|
||||
return React.useCallback(() => {}, []);
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import getErrorMessage from 'lib/errors/getErrorMessage';
|
||||
import getErrorObj from 'lib/errors/getErrorObj';
|
||||
import getErrorObjPayload from 'lib/errors/getErrorObjPayload';
|
||||
import type * as mixpanel from 'lib/mixpanel';
|
||||
import useWeb3Wallet from 'lib/web3/useWallet';
|
||||
import useWalletReown from 'lib/web3/wallet/useWalletReown';
|
||||
import { toaster } from 'toolkit/chakra/toaster';
|
||||
import { YEAR } from 'toolkit/utils/consts';
|
||||
|
||||
@@ -49,7 +49,7 @@ function useSignInWithWallet({ onSuccess, onError, source = 'Login', isAuth, log
|
||||
const isConnectingWalletRef = React.useRef(false);
|
||||
|
||||
const apiFetch = useApiFetch();
|
||||
const web3Wallet = useWeb3Wallet({ source });
|
||||
const web3Wallet = useWalletReown({ source });
|
||||
const { signMessageAsync } = useSignMessage();
|
||||
const { switchChainAsync } = useSwitchChain();
|
||||
|
||||
|
||||
@@ -4,8 +4,7 @@ import React from 'react';
|
||||
import config from 'configs/app';
|
||||
import RewardsButton from 'ui/rewards/RewardsButton';
|
||||
import SearchBar from 'ui/snippets/searchBar/SearchBarDesktop';
|
||||
import UserProfileDesktop from 'ui/snippets/user/profile/UserProfileDesktop';
|
||||
import UserWalletDesktop from 'ui/snippets/user/wallet/UserWalletDesktop';
|
||||
import UserProfileDesktop from 'ui/snippets/user/UserProfileDesktop';
|
||||
|
||||
type Props = {
|
||||
renderSearchBar?: () => React.ReactNode;
|
||||
@@ -30,10 +29,7 @@ const HeaderDesktop = ({ renderSearchBar }: Props) => {
|
||||
{ config.UI.navigation.layout === 'vertical' && (
|
||||
<Box display="flex" gap={ 2 } flexShrink={ 0 }>
|
||||
{ config.features.rewards.isEnabled && <RewardsButton/> }
|
||||
{
|
||||
(config.features.account.isEnabled && <UserProfileDesktop/>) ||
|
||||
(config.features.blockchainInteraction.isEnabled && <UserWalletDesktop/>)
|
||||
}
|
||||
<UserProfileDesktop buttonVariant="header"/>
|
||||
</Box>
|
||||
) }
|
||||
</HStack>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import React from 'react';
|
||||
|
||||
import config from 'configs/app';
|
||||
import { useIsSticky } from 'toolkit/hooks/useIsSticky';
|
||||
import RewardsButton from 'ui/rewards/RewardsButton';
|
||||
import NetworkIcon from 'ui/snippets/networkLogo/NetworkIcon';
|
||||
import UserProfileMobile from 'ui/snippets/user/profile/UserProfileMobile';
|
||||
import UserProfileAuth0 from 'ui/snippets/user/profile/auth0/UserProfileMobile';
|
||||
import UserWalletMobile from 'ui/snippets/user/wallet/UserWalletMobile';
|
||||
|
||||
import RollupStageBadge from '../navigation/RollupStageBadge';
|
||||
@@ -13,6 +14,8 @@ import TestnetBadge from '../navigation/TestnetBadge';
|
||||
import SearchBarMobile from '../searchBar/SearchBarMobile';
|
||||
import Burger from './Burger';
|
||||
|
||||
const UserProfileDynamic = dynamic(() => import('ui/snippets/user/profile/dynamic/UserProfile'), { ssr: false });
|
||||
|
||||
type Props = {
|
||||
hideSearchButton?: boolean;
|
||||
onGoToSearchResults?: (searchTerm: string) => void;
|
||||
@@ -22,6 +25,23 @@ const HeaderMobile = ({ hideSearchButton, onGoToSearchResults }: Props) => {
|
||||
const ref = React.useRef<HTMLDivElement>(null);
|
||||
const isSticky = useIsSticky(ref, 5);
|
||||
|
||||
const userProfile = (() => {
|
||||
const accountFeature = config.features.account;
|
||||
if (accountFeature.isEnabled) {
|
||||
switch (accountFeature.authProvider) {
|
||||
case 'auth0':
|
||||
return <UserProfileAuth0/>;
|
||||
case 'dynamic':
|
||||
return <UserProfileDynamic/>;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (config.features.blockchainInteraction.isEnabled) {
|
||||
return <UserWalletMobile/>;
|
||||
}
|
||||
})();
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={ ref }
|
||||
@@ -54,9 +74,7 @@ const HeaderMobile = ({ hideSearchButton, onGoToSearchResults }: Props) => {
|
||||
<Flex columnGap={ 2 }>
|
||||
{ !hideSearchButton && <SearchBarMobile onGoToSearchResults={ onGoToSearchResults }/> }
|
||||
{ config.features.rewards.isEnabled && <RewardsButton/> }
|
||||
{ (config.features.account.isEnabled && <UserProfileMobile/>) ||
|
||||
(config.features.blockchainInteraction.isEnabled && <UserWalletMobile/>)
|
||||
}
|
||||
{ userProfile }
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Box>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Box, chakra, Flex } from '@chakra-ui/react';
|
||||
import { Box, chakra, Flex, Separator } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import config from 'configs/app';
|
||||
import useNavItems, { isGroupItem } from 'lib/hooks/useNavItems';
|
||||
import RewardsButton from 'ui/rewards/RewardsButton';
|
||||
import { CONTENT_MAX_WIDTH } from 'ui/shared/layout/utils';
|
||||
import useIsAuth from 'ui/snippets/auth/useIsAuth';
|
||||
import NetworkLogo from 'ui/snippets/networkLogo/NetworkLogo';
|
||||
import UserProfileDesktop from 'ui/snippets/user/profile/UserProfileDesktop';
|
||||
import UserWalletDesktop from 'ui/snippets/user/wallet/UserWalletDesktop';
|
||||
import UserProfileDesktop from 'ui/snippets/user/UserProfileDesktop';
|
||||
|
||||
import NavigationPromoBanner from '../promoBanner/NavigationPromoBanner';
|
||||
import RollupStageBadge from '../RollupStageBadge';
|
||||
@@ -15,8 +15,20 @@ import TestnetBadge from '../TestnetBadge';
|
||||
import NavLink from './NavLink';
|
||||
import NavLinkGroup from './NavLinkGroup';
|
||||
|
||||
const accountFeature = config.features.account;
|
||||
|
||||
const NavigationDesktop = () => {
|
||||
const { mainNavItems } = useNavItems();
|
||||
const { mainNavItems, accountNavItems } = useNavItems();
|
||||
const isAuth = useIsAuth();
|
||||
|
||||
const accountNavGroup = React.useMemo(() => {
|
||||
if (accountFeature.isEnabled && accountFeature.authProvider === 'dynamic' && isAuth) {
|
||||
return {
|
||||
text: 'Account',
|
||||
subItems: accountNavItems,
|
||||
};
|
||||
}
|
||||
}, [ accountNavItems, isAuth ]);
|
||||
|
||||
return (
|
||||
<Box borderColor="border.divider" borderBottomWidth="1px">
|
||||
@@ -31,8 +43,8 @@ const NavigationDesktop = () => {
|
||||
<NetworkLogo/>
|
||||
<TestnetBadge ml={ 3 }/>
|
||||
<RollupStageBadge ml={ 3 }/>
|
||||
<chakra.nav ml="auto" mr={ config.features.account.isEnabled || config.features.blockchainInteraction.isEnabled ? 8 : 0 }>
|
||||
<Flex as="ul" columnGap={ 3 }>
|
||||
<chakra.nav ml="auto">
|
||||
<Flex as="ul" columnGap={ 2 } alignItems="center">
|
||||
{ mainNavItems.map((item) => {
|
||||
if (isGroupItem(item)) {
|
||||
return <NavLinkGroup key={ item.text } item={ item }/>;
|
||||
@@ -40,15 +52,18 @@ const NavigationDesktop = () => {
|
||||
return <NavLink key={ item.text } item={ item } noIcon py={ 1.5 } w="fit-content"/>;
|
||||
}
|
||||
}) }
|
||||
{ accountNavGroup && (
|
||||
<>
|
||||
<Separator orientation="vertical" mx={ 0 } h={ 4 }/>
|
||||
<NavLinkGroup key={ accountNavGroup.text } item={ accountNavGroup }/>
|
||||
</>
|
||||
) }
|
||||
</Flex>
|
||||
</chakra.nav>
|
||||
<Flex gap={ 2 }>
|
||||
<Flex gap={ 2 } ml={ 8 } _empty={{ display: 'none' }}>
|
||||
<NavigationPromoBanner/>
|
||||
{ config.features.rewards.isEnabled && <RewardsButton size="sm"/> }
|
||||
{
|
||||
(config.features.account.isEnabled && <UserProfileDesktop buttonSize="sm"/>) ||
|
||||
(config.features.blockchainInteraction.isEnabled && <UserWalletDesktop buttonSize="sm"/>)
|
||||
}
|
||||
<UserProfileDesktop buttonSize="sm"/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Box>
|
||||
|
||||
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
@@ -0,0 +1,33 @@
|
||||
import { type ButtonProps } from '@chakra-ui/react';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
import config from 'configs/app';
|
||||
import UserProfileAuth0 from 'ui/snippets/user/profile/auth0/UserProfileDesktop';
|
||||
import UserWalletDesktop from 'ui/snippets/user/wallet/UserWalletDesktop';
|
||||
|
||||
const UserProfileDynamic = dynamic(() => import('ui/snippets/user/profile/dynamic/UserProfile'), { ssr: false });
|
||||
|
||||
interface Props {
|
||||
buttonSize?: ButtonProps['size'];
|
||||
buttonVariant?: ButtonProps['variant'];
|
||||
}
|
||||
|
||||
const UserProfileDesktop = ({ buttonSize, buttonVariant = 'header' }: Props) => {
|
||||
const accountFeature = config.features.account;
|
||||
if (accountFeature.isEnabled) {
|
||||
switch (accountFeature.authProvider) {
|
||||
case 'auth0':
|
||||
return <UserProfileAuth0 buttonSize={ buttonSize } buttonVariant={ buttonVariant }/>;
|
||||
case 'dynamic':
|
||||
return <UserProfileDynamic buttonSize={ buttonSize } buttonVariant={ buttonVariant }/>;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (config.features.blockchainInteraction.isEnabled) {
|
||||
return <UserWalletDesktop buttonSize={ buttonSize } buttonVariant={ buttonVariant }/>;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export default UserProfileDesktop;
|
||||
@@ -13,8 +13,8 @@ import { Button } from 'toolkit/chakra/button';
|
||||
import { Tooltip } from 'toolkit/chakra/tooltip';
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
|
||||
import UserIdenticon from '../UserIdenticon';
|
||||
import { getUserHandle } from './utils';
|
||||
import UserIdenticon from '../../UserIdenticon';
|
||||
import { getUserHandle } from '../common/utils';
|
||||
|
||||
interface Props {
|
||||
profileQuery: UseQueryResult<UserInfo, unknown>;
|
||||
@@ -50,7 +50,7 @@ const UserProfileButton = ({ profileQuery, size, variant, onClick, isPending, ..
|
||||
return (
|
||||
<HStack gap={ 2 }>
|
||||
<UserIdenticon address={ web3AccountWithDomain.address } isAutoConnectDisabled={ isAutoConnectDisabled }/>
|
||||
<Box display={{ base: 'none', md: 'block' }}>
|
||||
<Box display={{ base: 'none', md: 'block' }} maxW="200px" overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap">
|
||||
{ web3AccountWithDomain.domain || shortenString(web3AccountWithDomain.address) }
|
||||
</Box>
|
||||
</HStack>
|
||||
@@ -15,9 +15,9 @@ import { Hint } from 'toolkit/components/Hint/Hint';
|
||||
import { TruncatedText } from 'toolkit/components/truncation/TruncatedText';
|
||||
import useLogout from 'ui/snippets/auth/useLogout';
|
||||
|
||||
import UserWalletAutoConnectAlert from '../UserWalletAutoConnectAlert';
|
||||
import UserWalletAutoConnectAlert from '../../UserWalletAutoConnectAlert';
|
||||
import UserProfileContentWallet from '../common/UserProfileContentWallet';
|
||||
import UserProfileContentNavLink from './UserProfileContentNavLink';
|
||||
import UserProfileContentWallet from './UserProfileContentWallet';
|
||||
|
||||
const navLinks: Array<NavLink> = [
|
||||
{
|
||||
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,68 @@
|
||||
import { type ButtonProps } from '@chakra-ui/react';
|
||||
import { DynamicConnectButton, DynamicUserProfile, useDynamicContext, useIsLoggedIn } from '@dynamic-labs/sdk-react-core';
|
||||
import React from 'react';
|
||||
|
||||
import { useMarketplaceContext } from 'lib/contexts/marketplace';
|
||||
import useWalletDynamic from 'lib/web3/wallet/useWalletDynamic';
|
||||
|
||||
import UserProfileButton from './UserProfileButton';
|
||||
import UserProfilePopover from './UserProfilePopover';
|
||||
|
||||
interface Props {
|
||||
buttonSize?: ButtonProps['size'];
|
||||
buttonVariant?: ButtonProps['variant'];
|
||||
}
|
||||
|
||||
const UserProfile = ({ buttonSize, buttonVariant = 'header' }: Props) => {
|
||||
const isLoggedIn = useIsLoggedIn();
|
||||
const { user, authMode } = useDynamicContext();
|
||||
const wallet = useWalletDynamic({ source: isLoggedIn ? 'Profile dropdown' : 'Login' });
|
||||
const { isAutoConnectDisabled } = useMarketplaceContext();
|
||||
|
||||
const handleOpenModal = React.useCallback(() => {
|
||||
wallet.openModal();
|
||||
}, [ wallet ]);
|
||||
|
||||
if (isLoggedIn && authMode === 'connect-and-sign') {
|
||||
return (
|
||||
<>
|
||||
<UserProfileButton
|
||||
email={ user?.email }
|
||||
size={ buttonSize }
|
||||
variant={ buttonVariant }
|
||||
onClick={ handleOpenModal }
|
||||
selected
|
||||
/>
|
||||
<DynamicUserProfile variant="modal"/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (wallet.isConnected) {
|
||||
return (
|
||||
<>
|
||||
<UserProfilePopover isAutoConnectDisabled={ isAutoConnectDisabled }>
|
||||
<UserProfileButton
|
||||
email={ user?.email }
|
||||
size={ buttonSize }
|
||||
variant={ buttonVariant }
|
||||
selected
|
||||
/>
|
||||
</UserProfilePopover>
|
||||
<DynamicUserProfile variant="modal"/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DynamicConnectButton>
|
||||
<UserProfileButton
|
||||
as="div"
|
||||
size={ buttonSize }
|
||||
variant={ buttonVariant }
|
||||
/>
|
||||
</DynamicConnectButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(UserProfile);
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Box, HStack } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import { useMarketplaceContext } from 'lib/contexts/marketplace';
|
||||
import shortenString from 'lib/shortenString';
|
||||
import useAccountWithDomain from 'lib/web3/useAccountWithDomain';
|
||||
import { Button, type ButtonProps } from 'toolkit/chakra/button';
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
|
||||
import UserIdenticon from '../../UserIdenticon';
|
||||
import { getUserHandle } from '../common/utils';
|
||||
|
||||
interface Props extends ButtonProps {
|
||||
email?: string;
|
||||
}
|
||||
|
||||
const UserProfileButton = ({ selected, email, ...rest }: Props) => {
|
||||
const { isAutoConnectDisabled } = useMarketplaceContext();
|
||||
const accountWithDomain = useAccountWithDomain(true);
|
||||
|
||||
const isLoading = accountWithDomain.isLoading;
|
||||
|
||||
const content = (() => {
|
||||
if (selected && !isLoading) {
|
||||
return accountWithDomain.address ? (
|
||||
<HStack gap={ 2 }>
|
||||
<UserIdenticon address={ accountWithDomain.address } isAutoConnectDisabled={ isAutoConnectDisabled }/>
|
||||
<Box display={{ base: 'none', md: 'block' }} maxW="200px" overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap">
|
||||
{ accountWithDomain.domain || shortenString(accountWithDomain.address) }
|
||||
</Box>
|
||||
</HStack>
|
||||
) : (
|
||||
<HStack gap={ 2 }>
|
||||
<IconSvg name="profile" boxSize={ 5 }/>
|
||||
<Box display={{ base: 'none', md: 'block' }}>{ email ? getUserHandle(email) : 'My profile' }</Box>
|
||||
</HStack>
|
||||
);
|
||||
}
|
||||
|
||||
return 'Log in';
|
||||
})();
|
||||
|
||||
return (
|
||||
<Button
|
||||
px={{ base: 2.5, lg: 3 }}
|
||||
selected={ selected }
|
||||
highlighted={ isAutoConnectDisabled }
|
||||
fontWeight={ selected ? 700 : undefined }
|
||||
loading={ isLoading }
|
||||
{ ...rest }
|
||||
>
|
||||
{ content }
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(UserProfileButton);
|
||||
@@ -0,0 +1,43 @@
|
||||
import { DynamicConnectButton, useAuthenticateConnectedUser } from '@dynamic-labs/sdk-react-core';
|
||||
import React from 'react';
|
||||
|
||||
import { Button } from 'toolkit/chakra/button';
|
||||
import { PopoverBody, PopoverContent, PopoverRoot, PopoverTrigger } from 'toolkit/chakra/popover';
|
||||
|
||||
import UserWalletAutoConnectAlert from '../../UserWalletAutoConnectAlert';
|
||||
import UserProfileContentWallet from '../common/UserProfileContentWallet';
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
isAutoConnectDisabled: boolean;
|
||||
}
|
||||
|
||||
const UserProfilePopover = ({ children, isAutoConnectDisabled }: Props) => {
|
||||
|
||||
const { authenticateUser } = useAuthenticateConnectedUser();
|
||||
|
||||
return (
|
||||
<PopoverRoot positioning={{ placement: 'bottom-end' }}>
|
||||
<PopoverTrigger>
|
||||
{ children }
|
||||
</PopoverTrigger>
|
||||
<PopoverContent w="280px">
|
||||
<PopoverBody
|
||||
css={{
|
||||
'& .dynamic-login-button': {
|
||||
width: '100%',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{ isAutoConnectDisabled && <UserWalletAutoConnectAlert/> }
|
||||
<UserProfileContentWallet/>
|
||||
<DynamicConnectButton buttonClassName="dynamic-login-button">
|
||||
<Button as="div" mt={ 3 } onClick={ authenticateUser } size="sm" w="100%" >Log in</Button>
|
||||
</DynamicConnectButton>
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
</PopoverRoot>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(UserProfilePopover);
|
||||
@@ -1,12 +1,25 @@
|
||||
import React from 'react';
|
||||
|
||||
import config from 'configs/app';
|
||||
import { Alert } from 'toolkit/chakra/alert';
|
||||
import { Button } from 'toolkit/chakra/button';
|
||||
import { useDisclosure } from 'toolkit/hooks/useDisclosure';
|
||||
import AuthModal from 'ui/snippets/auth/AuthModal';
|
||||
import useLinkEmail from 'ui/snippets/auth/linkEmail/useLinkEmail';
|
||||
|
||||
const feature = config.features.account;
|
||||
|
||||
const VerifiedAddressesEmailAlert = () => {
|
||||
const authModal = useDisclosure();
|
||||
const linkEmail = useLinkEmail();
|
||||
|
||||
const handleButtonClick = React.useCallback(() => {
|
||||
if (feature.isEnabled && feature.authProvider === 'dynamic') {
|
||||
linkEmail();
|
||||
} else {
|
||||
authModal.onOpen();
|
||||
}
|
||||
}, [ authModal, linkEmail ]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -19,7 +32,7 @@ const VerifiedAddressesEmailAlert = () => {
|
||||
}}
|
||||
>
|
||||
You need a valid email address to verify contracts. Please add your email to your account.
|
||||
<Button variant="outline" size="sm" onClick={ authModal.onOpen }>Add email</Button>
|
||||
<Button variant="outline" size="sm" onClick={ handleButtonClick }>Add email</Button>
|
||||
</Alert>
|
||||
{ authModal.open && <AuthModal initialScreen={{ type: 'email', isAuth: true }} onClose={ authModal.onClose }/> }
|
||||
</>
|
||||
|
||||
@@ -1,12 +1,25 @@
|
||||
import React from 'react';
|
||||
|
||||
import config from 'configs/app';
|
||||
import { Alert } from 'toolkit/chakra/alert';
|
||||
import { Button } from 'toolkit/chakra/button';
|
||||
import { useDisclosure } from 'toolkit/hooks/useDisclosure';
|
||||
import AuthModal from 'ui/snippets/auth/AuthModal';
|
||||
import useLinkEmail from 'ui/snippets/auth/linkEmail/useLinkEmail';
|
||||
|
||||
const feature = config.features.account;
|
||||
|
||||
const WatchlistEmailAlert = () => {
|
||||
const authModal = useDisclosure();
|
||||
const linkEmail = useLinkEmail();
|
||||
|
||||
const handleButtonClick = React.useCallback(() => {
|
||||
if (feature.isEnabled && feature.authProvider === 'dynamic') {
|
||||
linkEmail();
|
||||
} else {
|
||||
authModal.onOpen();
|
||||
}
|
||||
}, [ authModal, linkEmail ]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -17,7 +30,7 @@ const WatchlistEmailAlert = () => {
|
||||
mb={ 6 }
|
||||
>
|
||||
To receive notifications you need to add an email to your profile.
|
||||
<Button variant="outline" size="sm" onClick={ authModal.onOpen }>Add email</Button>
|
||||
<Button variant="outline" size="sm" onClick={ handleButtonClick }>Add email</Button>
|
||||
</Alert>
|
||||
{ authModal.open && <AuthModal initialScreen={{ type: 'email', isAuth: true }} onClose={ authModal.onClose }/> }
|
||||
</>
|
||||
|
||||