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
2 changes: 1 addition & 1 deletion demo/vite-react-app-sfa/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"dependencies": {
"@react-oauth/google": "^0.12.1",
"@tanstack/react-query": "^5.74.11",
"@web3auth/auth": "^11.4.2",
"@web3auth/auth": "^11.6.0",
"@web3auth/modal": "file:../../packages/modal",
"@web3auth/sign-in-with-web3": "^6.1.0",
"ethers": "^6.13.4",
Expand Down
2 changes: 1 addition & 1 deletion demo/vue-app-new/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"@toruslabs/vue-components": "^8.1.2",
"@toruslabs/vue-icons": "^9.0.0",
"@wagmi/vue": "^0.5.1",
"@web3auth/auth": "^11.5.0",
"@web3auth/auth": "^11.6.0",
"@web3auth/modal": "file:../../packages/modal",
"@web3auth/no-modal": "file:../../packages/no-modal",
"@web3auth/sign-in-with-web3": "^6.1.0",
Expand Down
2 changes: 1 addition & 1 deletion demo/wagmi-react-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"private": true,
"dependencies": {
"@tanstack/react-query": "^5.95.2",
"@web3auth/auth": "^11.5.0",
"@web3auth/auth": "^11.6.0",
"@web3auth/modal": "file:../../packages/modal",
"react": "^19.2.4",
"react-dom": "^19.2.4",
Expand Down
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@
"@solana-program/token-2022": "^0.9.0"
},
"dependencies": {
"@web3auth/auth": "^11.5.0",
"@web3auth/auth": "^11.6.0",
"@web3auth/ws-embed": "file:web3auth-ws-embed-5.6.5.tgz"
}
}
2 changes: 1 addition & 1 deletion packages/modal/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@
"@hcaptcha/react-hcaptcha": "^2.0.2",
"@toruslabs/base-controllers": "^9.5.0",
"@toruslabs/http-helpers": "^9.0.0",
"@web3auth/auth": "^11.5.0",
"@web3auth/auth": "^11.6.0",
"@web3auth/no-modal": "^10.15.0",
"@web3auth/ws-embed": "file:../../web3auth-ws-embed-5.6.5.tgz",
"bowser": "^2.14.1",
Expand Down
58 changes: 50 additions & 8 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 = 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,9 @@ export class Web3Auth extends Web3AuthNoModal implements IWeb3AuthModal {
this.analytics.setGlobalProperties({ team_id: projectConfig.teamId });
trackData = this.getInitializationTrackData();

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 +139,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 @@ -186,33 +199,44 @@ export class Web3Auth extends Web3AuthNoModal implements IWeb3AuthModal {
return new Promise((resolve, reject) => {
// remove all listeners when promise is resolved or rejected.
// this is to prevent memory leaks if user clicks connect button multiple times.
const handleConnected = () => {
const handleCompletion = () => {
this.removeListener(CONNECTOR_EVENTS.ERRORED, handleError);
this.removeListener(LOGIN_MODAL_EVENTS.MODAL_VISIBILITY, handleVisibility);
if (this.consentRequired) {
this.removeListener(CONNECTOR_EVENTS.CONSENT_ACCEPTED, handleCompletion);
} else if (this.coreOptions.initialAuthenticationMode === CONNECTOR_INITIAL_AUTHENTICATION_MODE.CONNECT_AND_SIGN) {
this.removeListener(CONNECTOR_EVENTS.AUTHORIZED, handleCompletion);
} else {
this.removeListener(CONNECTOR_EVENTS.CONNECTED, handleCompletion);
}
return resolve(this.connection);
};

const handleError = (err: unknown) => {
this.removeListener(CONNECTOR_EVENTS.CONNECTED, handleConnected);
this.removeListener(CONNECTOR_EVENTS.AUTHORIZED, handleConnected);
this.removeListener(CONNECTOR_EVENTS.CONNECTED, handleCompletion);
this.removeListener(CONNECTOR_EVENTS.AUTHORIZED, handleCompletion);
this.removeListener(CONNECTOR_EVENTS.CONSENT_ACCEPTED, handleCompletion);
this.removeListener(LOGIN_MODAL_EVENTS.MODAL_VISIBILITY, handleVisibility);
return reject(err);
};

const handleVisibility = (visibility: boolean) => {
// modal is closed but user is not connected to any wallet.
if (!visibility && !CONNECTED_STATUSES.includes(this.status)) {
this.removeListener(CONNECTOR_EVENTS.CONNECTED, handleConnected);
this.removeListener(CONNECTOR_EVENTS.CONNECTED, handleCompletion);
this.removeListener(CONNECTOR_EVENTS.ERRORED, handleError);
this.removeListener(CONNECTOR_EVENTS.AUTHORIZED, handleConnected);
this.removeListener(CONNECTOR_EVENTS.AUTHORIZED, handleCompletion);
this.removeListener(CONNECTOR_EVENTS.CONSENT_ACCEPTED, handleCompletion);
return reject(new Error("User closed the modal"));
}
};

if (this.coreOptions.initialAuthenticationMode === CONNECTOR_INITIAL_AUTHENTICATION_MODE.CONNECT_AND_SIGN) {
this.once(CONNECTOR_EVENTS.AUTHORIZED, handleConnected);
if (this.consentRequired) {
this.once(CONNECTOR_EVENTS.CONSENT_ACCEPTED, handleCompletion);
} else if (this.coreOptions.initialAuthenticationMode === CONNECTOR_INITIAL_AUTHENTICATION_MODE.CONNECT_AND_SIGN) {
this.once(CONNECTOR_EVENTS.AUTHORIZED, handleCompletion);
} else {
this.once(CONNECTOR_EVENTS.CONNECTED, handleConnected);
this.once(CONNECTOR_EVENTS.CONNECTED, handleCompletion);
}

this.once(CONNECTOR_EVENTS.ERRORED, handleError);
Expand Down Expand Up @@ -696,6 +720,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
15 changes: 15 additions & 0 deletions packages/modal/src/react/context/Web3AuthInnerContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,19 @@ export function Web3AuthInnerProvider(params: PropsWithChildren<Web3AuthProvider
setIsAuthorized(true);
}
};
const consentAcceptedListener = () => {
setStatus(web3Auth.status);
if (web3Auth.status === CONNECTOR_STATUS.CONNECTED || web3Auth.status === CONNECTOR_STATUS.AUTHORIZED) {
setIsInitialized(true);
setIsConnected(true);
setConnection(web3Auth.connection);
setChainId(web3Auth.currentChainId);
setChainNamespace(web3Auth.currentChain?.chainNamespace ?? null);
if (web3Auth.status === CONNECTOR_STATUS.AUTHORIZED) {
setIsAuthorized(true);
}
}
};
const disconnectedListener = () => {
setStatus(web3Auth.status);
setIsConnected(false);
Expand Down Expand Up @@ -137,6 +150,7 @@ export function Web3AuthInnerProvider(params: PropsWithChildren<Web3AuthProvider
web3Auth.on(CONNECTOR_EVENTS.READY, readyListener);
web3Auth.on(CONNECTOR_EVENTS.CONNECTED, connectedListener);
web3Auth.on(CONNECTOR_EVENTS.AUTHORIZED, authorizedListener);
web3Auth.on(CONNECTOR_EVENTS.CONSENT_ACCEPTED, consentAcceptedListener);
web3Auth.on(CONNECTOR_EVENTS.DISCONNECTED, disconnectedListener);
web3Auth.on(CONNECTOR_EVENTS.CONNECTING, connectingListener);
web3Auth.on(CONNECTOR_EVENTS.ERRORED, errorListener);
Expand All @@ -150,6 +164,7 @@ export function Web3AuthInnerProvider(params: PropsWithChildren<Web3AuthProvider
web3Auth.removeListener(CONNECTOR_EVENTS.READY, readyListener);
web3Auth.removeListener(CONNECTOR_EVENTS.CONNECTED, connectedListener);
web3Auth.removeListener(CONNECTOR_EVENTS.AUTHORIZED, authorizedListener);
web3Auth.removeListener(CONNECTOR_EVENTS.CONSENT_ACCEPTED, consentAcceptedListener);
web3Auth.removeListener(CONNECTOR_EVENTS.DISCONNECTED, disconnectedListener);
web3Auth.removeListener(CONNECTOR_EVENTS.CONNECTING, connectingListener);
web3Auth.removeListener(CONNECTOR_EVENTS.ERRORED, errorListener);
Expand Down
85 changes: 83 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 @@ -112,6 +112,73 @@ 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--flex w3a--w-full w3a--flex-col w3a--items-center w3a--gap-y-6 w3a--mt-8">
<div className="w3a--flex w3a--items-center w3a--justify-center w3a--p-3 w3a--bg-app-gray-100 dark:w3a--bg-app-gray-800 w3a--rounded-full w3a--text-app-gray-600 dark:w3a--text-app-white">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" className="w3a--size-10">
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5.586a1 1 0 0 1 .707.293l5.414 5.414a1 1 0 0 1 .293.707V19a2 2 0 0 1-2 2"
></path>
</svg>
</div>
<div className="w3a--w-full w3a--px-8 w3a--text-center w3a--text-app-gray-900 dark:w3a--text-app-white">
{t("modal.consent.description", { defaultValue: "To proceed, please accept the terms and privacy policy" })}
</div>
{(tncLink || privacyPolicy) && (
<div className="w3a--flex w3a--w-full w3a--flex-col w3a--gap-y-2">
{tncLink && (
<a
href={tncLink}
target="_blank"
rel="noopener noreferrer"
className="w3a--btn !w3a--text-sm w3a--font-light !w3a--justify-start w3a--rounded-full w3a--border-app-gray-50 w3a--bg-app-gray-50 w3a--p-3 w3a--text-left w3a--text-app-gray-700 hover:w3a--border-app-gray-200 hover:w3a--bg-app-gray-200 hover:w3a--text-app-gray-900 dark:w3a--border-app-gray-800 dark:w3a--bg-app-gray-800 dark:w3a--text-app-white dark:hover:w3a--border-app-gray-600 dark:hover:w3a--bg-app-gray-600"
>
{t("modal.consent.tnc", { defaultValue: "Terms of service" })}
</a>
)}
{privacyPolicy && (
<a
href={privacyPolicy}
target="_blank"
rel="noopener noreferrer"
className="w3a--btn !w3a--text-sm w3a--font-light !w3a--justify-start w3a--rounded-full w3a--border-app-gray-50 w3a--bg-app-gray-50 w3a--p-3 w3a--text-left w3a--text-app-gray-700 hover:w3a--border-app-gray-200 hover:w3a--bg-app-gray-200 hover:w3a--text-app-gray-900 dark:w3a--border-app-gray-800 dark:w3a--bg-app-gray-800 dark:w3a--text-app-white dark:hover:w3a--border-app-gray-600 dark:hover:w3a--bg-app-gray-600"
>
{t("modal.consent.privacy", { defaultValue: "Privacy Policy" })}
</a>
)}
</div>
)}
<div className="w3a--flex w3a--w-full w3a--gap-x-2">
<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>
<button
type="button"
disabled={isSubmitting}
onClick={handleAccept}
className="w3a--btn w3a--rounded-full w3a--border-app-primary-600 w3a--bg-app-primary-600 hover:w3a--border-app-primary-700 hover:w3a--bg-app-primary-700 disabled:w3a--opacity-60 dark:w3a--border-app-primary-600 dark:w3a--bg-app-primary-600 dark:hover:w3a--border-app-primary-700 dark:hover:w3a--bg-app-primary-700"
>
<p className="w3a--text-app-onPrimary">{t("modal.consent.accept", { defaultValue: "Accept" })}</p>
</button>
</div>
</div>
);
}

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

const isConnectedAccordingToAuthenticationMode = useMemo(
Expand All @@ -150,8 +221,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} />}

{isConnectedAccordingToAuthenticationMode && !hideSuccessScreen && <ConnectedStatus message={message} />}
Expand All @@ -165,6 +244,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 @@ -12,6 +12,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" | "connector">;
Expand Down
Loading
Loading