Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
44 changes: 42 additions & 2 deletions packages/modal/src/ui/components/Loader/Loader.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { WALLET_CONNECTOR_TYPE } from "@web3auth/no-modal";
import { log, WALLET_CONNECTOR_TYPE } from "@web3auth/no-modal";
import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";

import { MODAL_STATUS } from "../../interfaces";
import i18n from "../../localeImport";
import Image from "../Image";
import SpinnerLoader from "../SpinnerLoader";
import { AuthorizingStatusType, ConnectedStatusType, ConnectingStatusType, ErroredStatusType, LoaderProps } from "./Loader.type";
import { AuthorizingStatusType, BlockedStatusType, ConnectedStatusType, ConnectingStatusType, ErroredStatusType, LoaderProps } from "./Loader.type";

/**
* ConnectingStatus component
Expand Down Expand Up @@ -83,6 +83,37 @@ function ErroredStatus(props: ErroredStatusType) {
);
}

function BlockedStatus(props: BlockedStatusType) {
const { primaryMessage, secondaryMessage, buttonMessage } = props;

const handleChangeWallet = () => {
// TODO: wire up real action in a follow-up commit
log.info("change wallet");
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocked user button has no functional action

High Severity

The handleChangeWallet callback in BlockedStatus only calls log.info("change wallet") and performs no actual action. This button is rendered prominently to blocked users (with default text "Change wallet") but clicking it does nothing. Combined with the fact that onCloseLoader for BLOCKED status hides the modal without resetting status to INITIALIZED, the user is left in a dead-end state with no functional escape path other than a full page reload.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e27d7d9. Configure here.


return (
<div className="w3a--flex w3a--flex-col w3a--items-center w3a--gap-y-4">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20" className="w3a--error-logo">
<path
fill="currentColor"
fillRule="evenodd"
d="M18 10a8 8 0 1 1-16.001 0A8 8 0 0 1 18 10m-7 4a1 1 0 1 1-2 0 1 1 0 0 1 2 0m-1-9a1 1 0 0 0-1 1v4a1 1 0 1 0 2 0V6a1 1 0 0 0-1-1"
clipRule="evenodd"
/>
</svg>
<p className="w3a--text-center w3a--text-base w3a--font-semibold w3a--text-app-gray-900 dark:w3a--text-app-white">{primaryMessage}</p>
<p className="w3a--text-center w3a--text-sm w3a--text-app-gray-500 dark:w3a--text-app-gray-400">{secondaryMessage}</p>
<button
type="button"
onClick={handleChangeWallet}
className="w3a--rounded-xl w3a--bg-app-primary-600 w3a--px-6 w3a--py-3 w3a--text-center w3a--text-sm w3a--font-medium w3a--text-app-white hover:w3a--bg-app-primary-700"
>
{buttonMessage}
</button>
</div>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrong CSS class prefix breaks BlockedStatus styling

High Severity

The BlockedStatus component uses the w3a-- prefix for all utility CSS classes (e.g., w3a--flex, w3a--text-center, w3a--bg-app-primary-600) and dark:w3a-- for dark mode. Every other component in this file and across the UI layer uses the wta: prefix for utility classes (e.g., wta:flex, wta:text-center) and wta:dark: for dark mode. The w3a-- prefix is only used in this codebase for specific named component classes like w3a--error-logo and w3a--btn, not for utility classes. This means the entire blocked user screen will render without layout, spacing, text styling, or button styling applied.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 2583007. Configure here.

);
}

function AuthorizingStatus(props: AuthorizingStatusType) {
const [t] = useTranslation(undefined, { i18n });
const { connector, externalWalletsConfig, handleMobileVerifyConnect } = props;
Expand Down Expand Up @@ -213,6 +244,7 @@ function Loader(props: LoaderProps) {
externalWalletsConfig,
handleMobileVerifyConnect,
hideSuccessScreen = false,
blockedUserConfig,
onAcceptConsent,
onDeclineConsent,
privacyPolicy,
Expand Down Expand Up @@ -255,6 +287,14 @@ function Loader(props: LoaderProps) {

{modalStatus === MODAL_STATUS.ERRORED && <ErroredStatus message={message} />}

{modalStatus === MODAL_STATUS.BLOCKED && blockedUserConfig && (
<BlockedStatus
primaryMessage={blockedUserConfig.primaryMessage}
secondaryMessage={blockedUserConfig.secondaryMessage}
buttonMessage={blockedUserConfig.buttonMessage}
/>
)}

{modalStatus === MODAL_STATUS.AUTHORIZING && (
<AuthorizingStatus
connector={connector}
Expand Down
5 changes: 4 additions & 1 deletion packages/modal/src/ui/components/Loader/Loader.type.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { BaseConnectorConfig, WALLET_CONNECTOR_TYPE } from "@web3auth/no-modal";

import type { ModalStatusType } from "../../interfaces";
import type { BlockedUserConfig, ModalStatusType } from "../../interfaces";

export interface LoaderProps {
externalWalletsConfig: Record<string, BaseConnectorConfig>;
Expand All @@ -12,6 +12,7 @@ export interface LoaderProps {
isConnectAndSignAuthenticationMode: boolean;
handleMobileVerifyConnect: (params: { connector: WALLET_CONNECTOR_TYPE }) => void;
hideSuccessScreen?: boolean;
blockedUserConfig?: BlockedUserConfig;
onAcceptConsent?: () => void | Promise<void>;
onDeclineConsent?: () => void | Promise<void>;
privacyPolicy?: string;
Expand All @@ -25,3 +26,5 @@ export type ConnectedStatusType = Pick<LoaderProps, "message">;
export type ErroredStatusType = Pick<LoaderProps, "message">;

export type AuthorizingStatusType = Pick<LoaderProps, "connector" | "externalWalletsConfig" | "handleMobileVerifyConnect">;

export type BlockedStatusType = BlockedUserConfig;
1 change: 1 addition & 0 deletions packages/modal/src/ui/containers/Root/Root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ function RootContent(props: RootProps) {
externalWalletsConfig={modalState.externalWalletsConfig}
handleMobileVerifyConnect={handleMobileVerifyConnect}
hideSuccessScreen={hideSuccessScreen}
blockedUserConfig={modalState.blockedUserConfig}
onAcceptConsent={handleAcceptConsent}
onDeclineConsent={handleDeclineConsent}
privacyPolicy={privacyPolicy}
Expand Down
8 changes: 8 additions & 0 deletions packages/modal/src/ui/containers/Widget/Widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,21 @@ function WidgetContent() {
postLoadingMessage: "",
});
}
if (modalState.status === MODAL_STATUS.BLOCKED) {
setModalState({
...modalState,
modalVisibility: false,
externalWalletsVisibility: false,
});
}
};

const showCloseIcon = useMemo(() => {
return (
modalState.status === MODAL_STATUS.INITIALIZED ||
modalState.status === MODAL_STATUS.CONNECTED ||
modalState.status === MODAL_STATUS.ERRORED ||
modalState.status === MODAL_STATUS.BLOCKED ||
modalState.status === MODAL_STATUS.AUTHORIZED
);
}, [modalState.status]);
Expand Down
20 changes: 20 additions & 0 deletions packages/modal/src/ui/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,16 @@ export interface UIConfig extends CoreUIConfig, LoginModalConfig {
*/
hideSuccessScreen?: boolean;

/**
* Configuration for the blocked user screen shown when a user is blocked by the dapp.
* All fields are optional and have defaults.
*/
blockedUserConfig?: {
primaryMessage?: string;
secondaryMessage?: string;
buttonMessage?: string;
};

connectorListener: SafeEventEmitter<Web3AuthNoModalEvents>;
}

Expand Down Expand Up @@ -122,6 +132,7 @@ export const MODAL_STATUS = {
CONNECTED: "connected",
CONNECTING: "connecting",
ERRORED: "errored",
BLOCKED: "blocked",
AUTHORIZING: "authorizing",
AUTHORIZED: "authorized",
CONSENT_REQUIRING: "consent_requiring",
Expand Down Expand Up @@ -206,6 +217,15 @@ export interface ModalState {
// Config State - set during initialization, rarely changes
socialLoginsConfig: SocialLoginsConfig;
externalWalletsConfig: Record<string, BaseConnectorConfig>;

// Blocked user state
blockedUserConfig?: BlockedUserConfig;
}

export interface BlockedUserConfig {
primaryMessage: string;
secondaryMessage: string;
buttonMessage: string;
}

export type SocialLoginEventType = { loginParams: ModalLoginParams };
Expand Down
12 changes: 12 additions & 0 deletions packages/modal/src/ui/loginModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,18 @@ export class LoginModal {
listener.on(CONNECTOR_EVENTS.ERRORED, (error: Web3AuthError, loginMode: LoginModeType) => {
log.error("error", error, error.message);
if (loginMode === LOGIN_MODE.NO_MODAL) return;
if (error.code === 5120) {
this.setState({
modalVisibility: true,
status: MODAL_STATUS.BLOCKED,
blockedUserConfig: {
primaryMessage: this.uiConfig.blockedUserConfig?.primaryMessage || "You cannot access the site.",
secondaryMessage: this.uiConfig.blockedUserConfig?.secondaryMessage || "Access to the site is restricted",
buttonMessage: this.uiConfig.blockedUserConfig?.buttonMessage || "Change wallet",
},
});
return;
}
if (error.code === 5000) {
if (this.uiConfig.displayErrorsOnModal)
this.setState({
Expand Down
5 changes: 5 additions & 0 deletions packages/no-modal/src/base/errors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ export class WalletLoginError extends Web3AuthError {
5117: "Unsupported operation",
5118: "useSFAKey flag is enabled but SFA key is not available",
5119: "User not logged in.",
5120: "User is blocked by the application",
};

public constructor(code: number, message?: string, cause?: unknown) {
Expand Down Expand Up @@ -193,6 +194,10 @@ export class WalletLoginError extends Web3AuthError {
public static userNotLoggedIn(extraMessage = "", cause?: unknown): IWeb3AuthError {
return WalletLoginError.fromCode(5119, extraMessage, cause);
}

public static userBlocked(extraMessage = "", cause?: unknown): IWeb3AuthError {
return WalletLoginError.fromCode(5120, extraMessage, cause);
}
}

export class WalletOperationsError extends Web3AuthError {
Expand Down