Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
97672c9
basic login button implementation
tom2drum Dec 30, 2025
c58e889
refactoring and add dynamic auth button to the other pages
tom2drum Jan 2, 2026
6a2fcd4
connect wallet without authenticating an user
tom2drum Jan 2, 2026
0cc2884
login and logout user in blockscout account
tom2drum Jan 2, 2026
a69329b
refactoring and RPC overrides
tom2drum Jan 2, 2026
fb6cbb5
authenticate user with connected wallet
tom2drum Jan 2, 2026
d2eb8c9
add ENS to profile button
tom2drum Jan 2, 2026
deecf82
override evm chains and fix contract write with waas wallet
tom2drum Jan 6, 2026
c00d9fc
disconnect wallet and fix docker build
tom2drum Jan 7, 2026
1ed5202
add dynamic imports to components that use web3 wallet packages
tom2drum Jan 7, 2026
ed46774
refactor user profile directory
tom2drum Jan 7, 2026
d895991
clean up
tom2drum Jan 9, 2026
1857636
Merge branch 'main' of github.com:blockscout/frontend into tom2drum/i…
tom2drum Jan 9, 2026
2f08863
Merge branch 'main' of github.com:blockscout/frontend into tom2drum/i…
tom2drum Jan 20, 2026
12e5b20
tweaks
tom2drum Jan 21, 2026
2122e93
update pw ci script
tom2drum Jan 21, 2026
8d0af50
fix "connect wallet" button behavior
tom2drum Jan 22, 2026
65246a4
handle linking email to profile with dynamic provider
tom2drum Jan 27, 2026
e5e2c1c
fix test
tom2drum Jan 27, 2026
b983322
async loading web3 hooks and remove dynamic loading from components t…
tom2drum Jan 28, 2026
4b6100f
fix pw tests
tom2drum Jan 29, 2026
919e5f8
add account navigation in horizontal layout when auth provider is dyn…
tom2drum Jan 29, 2026
b80b5b4
Merge branch 'main' of github.com:blockscout/frontend into tom2drum/i…
tom2drum Jan 29, 2026
98666c4
fix address verification modal
tom2drum Feb 4, 2026
0ced802
review fixes
tom2drum Feb 18, 2026
e902324
Merge branch 'main' of github.com:blockscout/frontend into tom2drum/i…
tom2drum Feb 18, 2026
a77e00a
spelling fixes
tom2drum Feb 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
38 changes: 30 additions & 8 deletions configs/app/features/account.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,45 @@
import type { Feature } from './types';
import type { AuthProvider } from 'types/client/account';

import app from '../app';
import services from '../services';
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({
Expand Down
42 changes: 31 additions & 11 deletions configs/app/features/blockchainInteraction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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({
Expand Down
3 changes: 2 additions & 1 deletion configs/app/features/rewards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion configs/envs/.env.main
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
7 changes: 3 additions & 4 deletions cspell.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down Expand Up @@ -105,7 +107,6 @@
"explorable",
"facebookexternalhit",
"favicons",
"fedoseev",
"filecoin",
"flashblock",
"flashblocks",
Expand All @@ -131,7 +132,6 @@
"inpage",
"internaltx",
"ipfs",
"isstuev",
"iszero",
"jazzicon",
"jfif",
Expand Down Expand Up @@ -252,7 +252,6 @@
"utka",
"utko",
"UUPS",
"vbaranov",
"VCALENDAR",
"vercel",
"verifreg",
Expand Down
2 changes: 1 addition & 1 deletion deploy/tools/envs-validator/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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)
Expand Down
22 changes: 22 additions & 0 deletions deploy/tools/envs-validator/schemas/features/account.ts
Original file line number Diff line number Diff line change
@@ -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 change: 1 addition & 0 deletions deploy/tools/envs-validator/schemas/features/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './account';
export * from './address3rdPartyWidgets';
export * from './ads';
export * from './apiDocs';
Expand Down
3 changes: 3 additions & 0 deletions deploy/tools/envs-validator/test/.env.alt
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 3 additions & 1 deletion docs/ENVS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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+ |

&nbsp;

Expand Down
3 changes: 3 additions & 0 deletions lib/api/services/general/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }`;
Expand Down
5 changes: 5 additions & 0 deletions lib/contexts/fallback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type React from 'react';

export const FallbackProvider = ({ children }: { children: React.ReactNode }) => {
return children;
};
2 changes: 1 addition & 1 deletion lib/mixpanel/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ? {
Expand Down
40 changes: 40 additions & 0 deletions lib/web3/account/useAccountDynamic.ts
Original file line number Diff line number Diff line change
@@ -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 ]);
}
17 changes: 17 additions & 0 deletions lib/web3/account/useAccountFallback.ts
Original file line number Diff line number Diff line change
@@ -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',
}), []);
}
13 changes: 10 additions & 3 deletions lib/web3/chains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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,
},
};
};

Expand Down Expand Up @@ -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 = (() => {
Expand All @@ -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 = (() => {
Expand Down
Loading
Loading