Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
33 changes: 33 additions & 0 deletions packages/modal/src/modalManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,13 @@ export class Web3Auth extends Web3AuthNoModal implements IWeb3AuthModal {
if (!this.options.uiConfig) this.options.uiConfig = {};
if (this.options.modalConfig) this.modalConfig = this.options.modalConfig;

const uiCfg = this.options.uiConfig;
this.consentRequired = Boolean("consentRequired" in uiCfg && uiCfg.consentRequired) && Boolean(uiCfg.privacyPolicy) && Boolean(uiCfg.tncLink);

if (this.consentRequired && this.status !== CONNECTOR_STATUS.NOT_READY) {
this.status = CONNECTOR_STATUS.CONSENT_REQUIRED;
}

log.info("modalConfig", this.modalConfig);
}

Expand Down Expand Up @@ -109,6 +116,11 @@ export class Web3Auth extends Web3AuthNoModal implements IWeb3AuthModal {
this.analytics.setGlobalProperties({ team_id: projectConfig.teamId });
trackData = this.getInitializationTrackData();

// TODO: remove override — testing consent flow
(this.options.uiConfig as Record<string, unknown>).consentRequired = true;
if (!this.options.uiConfig.privacyPolicy) this.options.uiConfig.privacyPolicy = "https://example.com/privacy";
if (!this.options.uiConfig.tncLink) this.options.uiConfig.tncLink = "https://example.com/terms";

// init login modal
const { filteredWalletRegistry, disabledExternalWallets } = this.filterWalletRegistry(walletRegistry, projectConfig);
this.loginModal = new LoginModal(
Expand All @@ -129,8 +141,11 @@ export class Web3Auth extends Web3AuthNoModal implements IWeb3AuthModal {
onExternalWalletLogin: this.onExternalWalletLogin,
onModalVisibility: this.onModalVisibility,
onMobileVerifyConnect: this.onMobileVerifyConnect,
onAcceptConsent: this.onAcceptConsent,
onDeclineConsent: this.onDeclineConsent,
}
);
this.consentRequired = this.loginModal.consentRequired;
await withAbort(() => this.loginModal.initModal(), signal);

// setup common JRPC provider
Expand Down Expand Up @@ -696,6 +711,24 @@ export class Web3Auth extends Web3AuthNoModal implements IWeb3AuthModal {
}
};

private onAcceptConsent = async (): Promise<void> => {
try {
await this.acceptConsent();
} catch (error) {
log.error("Error while accepting consent", error);
}
};

private onDeclineConsent = async (): Promise<void> => {
try {
await this.logout();
} catch (error) {
log.error("Error while declining consent", error);
} finally {
this.loginModal.closeModal();
}
};

private getChainNamespaces = (): ChainNamespaceType[] => {
return [...new Set(this.coreOptions.chains?.map((x) => x.chainNamespace) || [])];
};
Expand Down
61 changes: 59 additions & 2 deletions packages/modal/src/ui/components/Loader/Loader.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { WALLET_CONNECTOR_TYPE } from "@web3auth/no-modal";
import { useEffect, useMemo } from "react";
import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";

import { MODAL_STATUS } from "../../interfaces";
Expand Down Expand Up @@ -122,6 +122,49 @@ function AuthorizingStatus(props: AuthorizingStatusType) {
);
}

function ConsentRequiredStatus(props: { onAccept?: () => void; onDecline?: () => void; privacyPolicy?: string; tncLink?: string }) {
const { onAccept, onDecline, privacyPolicy, tncLink } = props;
const [t] = useTranslation(undefined, { i18n });
const [isSubmitting, setIsSubmitting] = useState(false);

const handleAccept = () => {
setIsSubmitting(true);
onAccept?.();
};

return (
<div className="w3a--my-4 w3a--flex w3a--w-full w3a--flex-col w3a--items-center w3a--gap-y-4">
<p className="w3a--text-center w3a--text-base w3a--font-semibold w3a--text-app-gray-900 dark:w3a--text-app-white">
{t("modal.consent.title", { defaultValue: "Terms and Conditions" })}
</p>
<p className="w3a--text-center w3a--text-sm w3a--text-app-gray-500 dark:w3a--text-app-gray-400">
{t("modal.consent.prefix", { defaultValue: "By continuing, you agree to the" })}{" "}
{tncLink && (
<a href={tncLink} className="w3a--text-app-primary-600 dark:w3a--text-app-primary-500">
{t("modal.consent.tnc", { defaultValue: "Terms and Conditions" })}{" "}
</a>
)}
{privacyPolicy && (
<>
{t("modal.consent.and", { defaultValue: "and" })}{" "}
<a href={privacyPolicy} className="w3a--text-app-primary-600 dark:w3a--text-app-primary-500">
{t("modal.consent.privacy", { defaultValue: "Privacy Policy" })}
</a>
</>
)}
</p>
<div className="w3a--flex w3a--w-full w3a--flex-col w3a--gap-y-2">
<button type="button" disabled={isSubmitting} onClick={handleAccept} className="w3a--btn w3a--rounded-full disabled:w3a--opacity-60">
<p className="w3a--text-app-gray-900 dark:w3a--text-app-white">{t("modal.consent.accept", { defaultValue: "Accept" })}</p>
</button>
<button type="button" disabled={isSubmitting} onClick={onDecline} className="w3a--btn w3a--rounded-full disabled:w3a--opacity-60">
<p className="w3a--text-app-gray-900 dark:w3a--text-app-white">{t("modal.consent.decline", { defaultValue: "Decline" })}</p>
</button>
</div>
</div>
);
}

/**
* Loader component
* @param props - LoaderProps
Expand All @@ -140,6 +183,10 @@ function Loader(props: LoaderProps) {
walletRegistry,
handleMobileVerifyConnect,
hideSuccessScreen = false,
onAcceptConsent,
onDeclineConsent,
privacyPolicy,
tncLink,
} = props;

const isConnectedAccordingToAuthenticationMode = useMemo(
Expand All @@ -162,8 +209,16 @@ function Loader(props: LoaderProps) {
}
}, [isConnectedAccordingToAuthenticationMode, hideSuccessScreen, onClose]);

const isConsent = modalStatus === MODAL_STATUS.CONSENT_REQUIRED;

return (
<div className="w3a--flex w3a--h-full w3a--flex-1 w3a--flex-col w3a--items-center w3a--justify-center w3a--gap-y-4">
<div
className={
isConsent
? "w3a--flex w3a--flex-col w3a--items-center w3a--justify-center w3a--gap-y-4"
: "w3a--flex w3a--h-full w3a--flex-1 w3a--flex-col w3a--items-center w3a--justify-center w3a--gap-y-4"
}
>
{modalStatus === MODAL_STATUS.CONNECTING && <ConnectingStatus connector={connector} connectorName={connectorName} appLogo={appLogo} />}

{isConnectedAccordingToAuthenticationMode && !hideSuccessScreen && <ConnectedStatus message={message} />}
Expand All @@ -178,6 +233,8 @@ function Loader(props: LoaderProps) {
handleMobileVerifyConnect={handleMobileVerifyConnect}
/>
)}

{isConsent && <ConsentRequiredStatus onAccept={onAcceptConsent} onDecline={onDeclineConsent} privacyPolicy={privacyPolicy} tncLink={tncLink} />}
</div>
);
}
Expand Down
4 changes: 4 additions & 0 deletions packages/modal/src/ui/components/Loader/Loader.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ export interface LoaderProps {
isConnectAndSignAuthenticationMode: boolean;
handleMobileVerifyConnect: (params: { connector: WALLET_CONNECTOR_TYPE }) => void;
hideSuccessScreen?: boolean;
onAcceptConsent?: () => void;
onDeclineConsent?: () => void;
privacyPolicy?: string;
tncLink?: string;
}

export type ConnectingStatusType = Pick<LoaderProps, "connectorName" | "appLogo" | "connector">;
Expand Down
27 changes: 24 additions & 3 deletions packages/modal/src/ui/containers/Root/Root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,17 @@ function RootContent(props: RootProps) {
const { onCloseLoader } = props;

const { modalState, shouldShowLoginPage, showPasswordLessInput, areSocialLoginsVisible } = useModalState();
const { appLogo, deviceDetails, uiConfig, isConnectAndSignAuthenticationMode, handleMobileVerifyConnect } = useWidget();
const {
appLogo,
deviceDetails,
uiConfig,
isConnectAndSignAuthenticationMode,
handleMobileVerifyConnect,
handleAcceptConsent,
handleDeclineConsent,
} = useWidget();
const { chainNamespaces, walletRegistry, privacyPolicy, tncLink, displayInstalledExternalWallets, hideSuccessScreen } = uiConfig;
const consentRequired = Boolean("consentRequired" in uiConfig && uiConfig.consentRequired);

const contentRef = useRef<HTMLDivElement>(null);
const [containerHeight, setContainerHeight] = useState(530);
Expand Down Expand Up @@ -188,6 +197,8 @@ function RootContent(props: RootProps) {
return modalState.status !== MODAL_STATUS.INITIALIZED;
}, [modalState.status]);

const isConsentRequired = modalState.status === MODAL_STATUS.CONSENT_REQUIRED;

return (
<div className="w3a--relative w3a--flex w3a--flex-col">
<div
Expand All @@ -197,7 +208,10 @@ function RootContent(props: RootProps) {
}}
>
<div className="w3a--modal-curtain" />
<div ref={contentRef} className={twMerge("w3a--relative w3a--flex w3a--flex-col w3a--p-6", isShowLoader ? "w3a--flex-1" : "w3a--flex-none")}>
<div
ref={contentRef}
className={twMerge("w3a--relative w3a--flex w3a--flex-col w3a--p-6", isShowLoader && !isConsentRequired ? "w3a--flex-1" : "w3a--flex-none")}
>
{/* Content */}
{isShowLoader ? (
<Loader
Expand All @@ -211,6 +225,10 @@ function RootContent(props: RootProps) {
walletRegistry={walletRegistry}
handleMobileVerifyConnect={handleMobileVerifyConnect}
hideSuccessScreen={hideSuccessScreen}
onAcceptConsent={handleAcceptConsent}
onDeclineConsent={handleDeclineConsent}
privacyPolicy={privacyPolicy}
tncLink={tncLink}
/>
) : (
<>
Expand All @@ -237,7 +255,10 @@ function RootContent(props: RootProps) {
)}

{/* Footer */}
<Footer privacyPolicy={privacyPolicy} termsOfService={tncLink} />
<Footer
privacyPolicy={!consentRequired && modalState.status !== MODAL_STATUS.CONSENT_REQUIRED ? privacyPolicy : undefined}
termsOfService={!consentRequired && modalState.status !== MODAL_STATUS.CONSENT_REQUIRED ? tncLink : undefined}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Footer links hidden on all screens when consent enabled

Medium Severity

When consentRequired is true, the !consentRequired condition is always false, causing privacyPolicy and tncLink to be undefined on every screen — including the initial login page, connecting screen, and success screen — not just the consent screen. The intent is to hide footer links only when the consent screen is active (where the links appear inline), but the condition short-circuits due to !consentRequired. The status check modalState.status !== MODAL_STATUS.CONSENT_REQUIRED alone would achieve the desired behavior.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 9006216. Configure here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is intended. we only show privacy and terms links in the footer if consent required is false.

/>

<RootBodySheets />
</div>
Expand Down
8 changes: 8 additions & 0 deletions packages/modal/src/ui/context/WidgetContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ type WidgetContextType = {
handleExternalWalletClick: (params: ExternalWalletEventType) => void;
handleMobileVerifyConnect: (params: { connector: WALLET_CONNECTOR_TYPE }) => void;
handleShowExternalWallets: (externalWalletsInitialized: boolean) => void;
handleAcceptConsent: () => void;
handleDeclineConsent: () => void;
closeModal: () => void;
};

Expand All @@ -25,6 +27,8 @@ type WidgetProviderProps = {
handleExternalWalletClick: (params: ExternalWalletEventType) => void;
handleMobileVerifyConnect: (params: { connector: WALLET_CONNECTOR_TYPE }) => void;
handleShowExternalWallets: (externalWalletsInitialized: boolean) => void;
handleAcceptConsent: () => void;
handleDeclineConsent: () => void;
closeModal: () => void;
};

Expand All @@ -39,6 +43,8 @@ export const WidgetProvider: FC<WidgetProviderProps> = ({
handleExternalWalletClick,
handleMobileVerifyConnect,
handleShowExternalWallets,
handleAcceptConsent,
handleDeclineConsent,
closeModal,
}) => {
const appLogo = useMemo(() => {
Expand All @@ -62,6 +68,8 @@ export const WidgetProvider: FC<WidgetProviderProps> = ({
handleExternalWalletClick,
handleMobileVerifyConnect,
handleShowExternalWallets,
handleAcceptConsent,
handleDeclineConsent,
closeModal,
}}
>
Expand Down
3 changes: 3 additions & 0 deletions packages/modal/src/ui/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ export interface LoginModalCallbacks {
}) => Promise<void>;
onModalVisibility: (visibility: boolean) => Promise<void>;
onMobileVerifyConnect: (params: { connector: WALLET_CONNECTOR_TYPE }) => Promise<void>;
onAcceptConsent: () => Promise<void>;
onDeclineConsent: () => Promise<void>;
}

export const LOGIN_MODAL_EVENTS = {
Expand All @@ -122,6 +124,7 @@ export const MODAL_STATUS = {
ERRORED: "errored",
AUTHORIZING: "authorizing",
AUTHORIZED: "authorized",
CONSENT_REQUIRED: "consent_required",
} as const;
export type ModalStatusType = (typeof MODAL_STATUS)[keyof typeof MODAL_STATUS];

Expand Down
26 changes: 26 additions & 0 deletions packages/modal/src/ui/loginModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ function createWrapperForEmbed(targetId: string) {
export class LoginModal {
private uiConfig: LoginModalProps;

private _consentRequired: boolean;

private stateEmitter: SafeEventEmitter<StateEmitterEvents>;

private callbacks: LoginModalCallbacks;
Expand Down Expand Up @@ -102,6 +104,9 @@ export class LoginModal {
if (!uiConfig.privacyPolicy) this.uiConfig.privacyPolicy = "";
if (!uiConfig.tncLink) this.uiConfig.tncLink = "";

this._consentRequired =
Boolean("consentRequired" in uiConfig && uiConfig.consentRequired) && Boolean(this.uiConfig.privacyPolicy) && Boolean(this.uiConfig.tncLink);

if (uiConfig.widgetType === WIDGET_TYPE.EMBED && !uiConfig.targetId) {
log.error("targetId is required for embed widget");
throw WalletInitializationError.invalidParams("targetId is required for embed widget");
Expand All @@ -113,6 +118,10 @@ export class LoginModal {
this.subscribeCoreEvents(this.uiConfig.connectorListener);
}

get consentRequired(): boolean {
return this._consentRequired;
}

get isDark(): boolean {
return this.uiConfig.mode === "dark" || (this.uiConfig.mode === "auto" && window.matchMedia("(prefers-color-scheme: dark)").matches);
}
Expand Down Expand Up @@ -260,6 +269,8 @@ export class LoginModal {
handleExternalWalletClick={this.handleExternalWalletClick}
handleMobileVerifyConnect={this.handleMobileVerifyConnect}
handleSocialLoginClick={this.handleSocialLoginClick}
handleAcceptConsent={this.handleAcceptConsent}
handleDeclineConsent={this.handleDeclineConsent}
closeModal={this.closeModal}
>
<Widget stateListener={this.stateEmitter} />
Expand Down Expand Up @@ -363,6 +374,18 @@ export class LoginModal {
}
};

private handleAcceptConsent = () => {
if (this.callbacks.onAcceptConsent) {
this.callbacks.onAcceptConsent();
}
};

private handleDeclineConsent = () => {
if (this.callbacks.onDeclineConsent) {
this.callbacks.onDeclineConsent();
}
};

private handleSocialLoginClick = (params: SocialLoginEventType) => {
log.info("social login clicked", params);
const { loginParams } = params;
Expand Down Expand Up @@ -459,5 +482,8 @@ export class LoginModal {
listener.on(CONNECTOR_EVENTS.AUTHORIZED, () => {
this.setState({ status: MODAL_STATUS.AUTHORIZED });
});
listener.on(CONNECTOR_EVENTS.CONSENT_REQUIRED, () => {
this.setState({ status: MODAL_STATUS.CONSENT_REQUIRED, modalVisibility: true });
});
};
}
2 changes: 2 additions & 0 deletions packages/no-modal/src/base/connector/connectorStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ export const CAN_AUTHORIZE_STATUSES: CONNECTOR_STATUS_TYPE[] = [
CONNECTOR_STATUS.AUTHORIZED,
CONNECTOR_STATUS.CONNECTED,
];

export const CAN_LOGOUT_STATUSES: CONNECTOR_STATUS_TYPE[] = [...CONNECTED_STATUSES, CONNECTOR_STATUS.CONSENT_REQUIRED];
1 change: 1 addition & 0 deletions packages/no-modal/src/base/connector/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const CONNECTOR_STATUS = {
ERRORED: "errored",
AUTHORIZED: "authorized",
AUTHORIZING: "authorizing",
CONSENT_REQUIRED: "consent_required",
} as const;

export const CONNECTOR_EVENTS = {
Expand Down
1 change: 1 addition & 0 deletions packages/no-modal/src/base/connector/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ export type ConnectorEvents = {
[CONNECTOR_EVENTS.CACHE_CLEAR]: () => void;
[CONNECTOR_EVENTS.CONNECTORS_UPDATED]: (data: { connectors: IConnector<unknown>[] }) => void;
[CONNECTOR_EVENTS.MFA_ENABLED]: (isMFAEnabled: boolean) => void;
[CONNECTOR_EVENTS.CONSENT_REQUIRED]: (data: CONNECTED_EVENT_DATA) => void;
};

export interface BaseConnectorConfig {
Expand Down
3 changes: 2 additions & 1 deletion packages/no-modal/src/base/core/IWeb3Auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,9 +228,10 @@ export interface IWeb3Auth extends IWeb3AuthCore {

export type SDK_CONNECTED_EVENT_DATA = CONNECTED_EVENT_DATA & { loginMode: LoginModeType };

export type Web3AuthNoModalEvents = Omit<ConnectorEvents, "connected" | "errored" | "ready"> & {
export type Web3AuthNoModalEvents = Omit<ConnectorEvents, "connected" | "errored" | "ready" | "consent_required"> & {
[CONNECTOR_EVENTS.READY]: () => void;
[CONNECTOR_EVENTS.CONNECTED]: (data: SDK_CONNECTED_EVENT_DATA) => void;
[CONNECTOR_EVENTS.CONSENT_REQUIRED]: (data: SDK_CONNECTED_EVENT_DATA) => void;
[CONNECTOR_EVENTS.ERRORED]: (error: Web3AuthError, loginMode: LoginModeType) => void;
MODAL_VISIBILITY: (visibility: boolean) => void;
};
Expand Down
Loading
Loading