From c255aeb91fda6578572e92d9f2e4d3abe8b94227 Mon Sep 17 00:00:00 2001 From: Alexander Kolberg Date: Sat, 11 Jul 2026 14:53:06 +0300 Subject: [PATCH 1/7] fix(connect): make Google WaaS login directly interactive --- .changeset/bright-mice-connect.md | 5 + .../ConnectButton/ConnectButton.tsx | 156 +++++++++++------- 2 files changed, 105 insertions(+), 56 deletions(-) create mode 100644 .changeset/bright-mice-connect.md diff --git a/.changeset/bright-mice-connect.md b/.changeset/bright-mice-connect.md new file mode 100644 index 000000000..72755de3a --- /dev/null +++ b/.changeset/bright-mice-connect.md @@ -0,0 +1,5 @@ +--- +'@0xsequence/connect': patch +--- + +Render the official Google WaaS sign-in button responsively, with a dark outlined theme and an icon fallback for compact layouts, so Google authentication remains visible and reliable without starting the connection loader before credentials are returned. Harmonize descriptive social sign-in controls with the official button's compact, centered pill layout. diff --git a/packages/connect/src/components/ConnectButton/ConnectButton.tsx b/packages/connect/src/components/ConnectButton/ConnectButton.tsx index a5e1b06d1..4492bf2cb 100644 --- a/packages/connect/src/components/ConnectButton/ConnectButton.tsx +++ b/packages/connect/src/components/ConnectButton/ConnectButton.tsx @@ -1,6 +1,6 @@ import { Card, ContextMenuIcon, Text, Tooltip, useTheme } from '@0xsequence/design-system' -import { GoogleLogin } from '@react-oauth/google' -import { useEffect, useState } from 'react' +import { GoogleLogin, type GoogleLoginProps } from '@react-oauth/google' +import { useEffect, useRef, useState } from 'react' import { appleAuthHelpers } from 'react-apple-signin-auth' import { getXIdToken } from '../../connectors/X/XAuth.js' @@ -9,9 +9,13 @@ import { useStorage, useStorageItem } from '../../hooks/useStorage.js' import type { ExtendedConnector, WalletProperties } from '../../types.js' const BUTTON_HEIGHT = '52px' -const BUTTON_HEIGHT_DESCRIPTIVE = '44px' +const BUTTON_HEIGHT_TEXT = '44px' +const BUTTON_HEIGHT_DESCRIPTIVE = '40px' +// Standard Google buttons have an intrinsic localized text width; narrow connector cells use the official icon variant. +const GOOGLE_STANDARD_BUTTON_MIN_WIDTH = 240 const iconSizeClasses = 'w-8 h-8' -const iconDescriptiveSizeClasses = 'w-6 h-6' +const iconTextSizeClasses = 'w-6 h-6' +const iconDescriptiveSizeClasses = 'w-5 h-5' export const getLogo = (theme: any, walletProps: WalletProperties) => theme === 'dark' @@ -40,13 +44,16 @@ export const ConnectButton = (props: ConnectButtonProps) => { return ( onConnect(connector)} - style={{ height: BUTTON_HEIGHT_DESCRIPTIVE }} + style={{ + height: isDescriptive ? BUTTON_HEIGHT_DESCRIPTIVE : BUTTON_HEIGHT_TEXT, + ...(isDescriptive ? { borderRadius: '9999px', gap: '10px', padding: '0 16px' } : {}) + }} > - - + + {buttonCopy} @@ -119,63 +126,93 @@ export const GoogleWaasConnectButton = ( setConnectingConnector?: (connector: ExtendedConnector | null) => void } ) => { - const { connector, onConnect, isDescriptive = false, label, setIsLoading, setConnectingConnector } = props + const { connector, onConnect, isDescriptive = false, setIsLoading, setConnectingConnector } = props const storage = useStorage() + const containerRef = useRef(null) + const googleButtonRef = useRef(null) + const [buttonWidth, setButtonWidth] = useState(0) + const [useIconButton, setUseIconButton] = useState(true) const { theme } = useTheme() - const walletProps = connector._wallet - const Logo = getLogo(theme, walletProps) + useEffect(() => { + const updateButtonWidth = () => { + const availableWidth = containerRef.current?.clientWidth ?? 0 + const nextButtonWidth = Math.min(400, Math.floor(availableWidth)) + setButtonWidth(currentWidth => (currentWidth === nextButtonWidth ? currentWidth : nextButtonWidth)) + } - const WaasLoginContent = () => { - const baseClasses = 'flex items-center w-full h-full bg-background-secondary absolute pointer-events-none top-0 right-0' - const layoutClasses = isDescriptive ? 'gap-3 justify-start px-4' : 'justify-center' - - const copy = walletProps?.ctaText || 'Continue with Google' - if (isDescriptive) { - return ( -
- - - {copy} - -
- ) + updateButtonWidth() + + if (typeof ResizeObserver === 'undefined') { + window.addEventListener('resize', updateButtonWidth) + return () => window.removeEventListener('resize', updateButtonWidth) } - return ( -
- -
- ) - } + const resizeObserver = new ResizeObserver(updateButtonWidth) + if (containerRef.current) { + resizeObserver.observe(containerRef.current) + } + + return () => resizeObserver.disconnect() + }, []) + + useEffect(() => { + setUseIconButton(buttonWidth < GOOGLE_STANDARD_BUTTON_MIN_WIDTH) + }, [buttonWidth, theme]) + + useEffect(() => { + if (useIconButton) { + return + } + + const buttonContainer = googleButtonRef.current + const availableWidth = containerRef.current?.clientWidth ?? 0 + if (!buttonContainer || availableWidth === 0) { + return + } + + const checkButtonOverflow = () => { + // GIS makes its iframe wider than the visible button to add click padding, so measure Google's immediate wrapper instead. + const renderedButton = buttonContainer.querySelector('iframe')?.parentElement + if (renderedButton && renderedButton.getBoundingClientRect().width > availableWidth + 1) { + setUseIconButton(true) + } + } + + checkButtonOverflow() + + const mutationObserver = new MutationObserver(checkButtonOverflow) + mutationObserver.observe(buttonContainer, { childList: true, subtree: true, attributes: true }) + + return () => mutationObserver.disconnect() + }, [buttonWidth, theme, useIconButton]) const buttonHeight = isDescriptive ? BUTTON_HEIGHT_DESCRIPTIVE : BUTTON_HEIGHT + const useSequenceShell = isDescriptive && !useIconButton + // GIS supports outline_dark, but @react-oauth/google's theme type has not caught up with the current API. + const googleButtonTheme = (theme === 'dark' ? 'outline_dark' : 'outline') as GoogleLoginProps['theme'] return ( - - { - setIsLoading?.(true) - setConnectingConnector?.(connector) - }} - > -
+
+
+ {buttonWidth > 0 && ( { if (credentialResponse.credential) { storage?.setItem(LocalStorageKey.WaasGoogleIdToken, credentialResponse.credential) @@ -188,11 +225,18 @@ export const GoogleWaasConnectButton = ( setConnectingConnector?.(null) }} /> + )} +
+ {useSequenceShell && ( + // Keep Google's iframe directly interactive while replacing only its outer border with Sequence's visual shell. +
+
- - - - + )} +
) } From 7e0561e63bb5fb53628f622b9c3e887c2a334bc6 Mon Sep 17 00:00:00 2001 From: Alexander Kolberg Date: Sat, 11 Jul 2026 15:16:24 +0300 Subject: [PATCH 2/7] fix(connect): address Google button review feedback --- .../ConnectButton/ConnectButton.tsx | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/connect/src/components/ConnectButton/ConnectButton.tsx b/packages/connect/src/components/ConnectButton/ConnectButton.tsx index 4492bf2cb..17eb3625f 100644 --- a/packages/connect/src/components/ConnectButton/ConnectButton.tsx +++ b/packages/connect/src/components/ConnectButton/ConnectButton.tsx @@ -130,11 +130,19 @@ export const GoogleWaasConnectButton = ( const storage = useStorage() const containerRef = useRef(null) const googleButtonRef = useRef(null) + const isMountedRef = useRef(true) const [buttonWidth, setButtonWidth] = useState(0) const [useIconButton, setUseIconButton] = useState(true) const { theme } = useTheme() + useEffect(() => { + isMountedRef.current = true + return () => { + isMountedRef.current = false + } + }, []) + useEffect(() => { const updateButtonWidth = () => { const availableWidth = containerRef.current?.clientWidth ?? 0 @@ -199,7 +207,7 @@ export const GoogleWaasConnectButton = ( className={`relative flex w-full items-center justify-center ${ useSequenceShell ? 'overflow-hidden rounded-full bg-background-secondary' : '' }`} - style={{ height: buttonHeight }} + style={{ height: buttonHeight, maxWidth: '400px', marginInline: 'auto' }} >
{buttonWidth > 0 && ( @@ -214,12 +222,21 @@ export const GoogleWaasConnectButton = ( logo_alignment={useIconButton ? undefined : 'center'} locale="en" onSuccess={credentialResponse => { + // GIS may finish after the modal has unmounted; ignore stale callbacks after dismissal. + if (!isMountedRef.current) { + return + } + if (credentialResponse.credential) { storage?.setItem(LocalStorageKey.WaasGoogleIdToken, credentialResponse.credential) onConnect(connector) } }} onError={() => { + if (!isMountedRef.current) { + return + } + console.log('Login Failed') setIsLoading?.(false) setConnectingConnector?.(null) From 567ac6fbe3b331e25875e6a547ce295cf8c323be Mon Sep 17 00:00:00 2001 From: Alexander Kolberg Date: Sat, 11 Jul 2026 21:37:29 +0300 Subject: [PATCH 3/7] refactor(connect): scope Google login styling --- .changeset/bright-mice-connect.md | 2 +- .../ConnectButton/ConnectButton.tsx | 20 ++++++++----------- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/.changeset/bright-mice-connect.md b/.changeset/bright-mice-connect.md index 72755de3a..fd175f7a9 100644 --- a/.changeset/bright-mice-connect.md +++ b/.changeset/bright-mice-connect.md @@ -2,4 +2,4 @@ '@0xsequence/connect': patch --- -Render the official Google WaaS sign-in button responsively, with a dark outlined theme and an icon fallback for compact layouts, so Google authentication remains visible and reliable without starting the connection loader before credentials are returned. Harmonize descriptive social sign-in controls with the official button's compact, centered pill layout. +Render the official Google WaaS sign-in button responsively, with a dark outlined theme and an icon fallback for compact layouts, so Google authentication remains visible and reliable without starting the connection loader before credentials are returned. diff --git a/packages/connect/src/components/ConnectButton/ConnectButton.tsx b/packages/connect/src/components/ConnectButton/ConnectButton.tsx index 17eb3625f..750178eba 100644 --- a/packages/connect/src/components/ConnectButton/ConnectButton.tsx +++ b/packages/connect/src/components/ConnectButton/ConnectButton.tsx @@ -9,13 +9,12 @@ import { useStorage, useStorageItem } from '../../hooks/useStorage.js' import type { ExtendedConnector, WalletProperties } from '../../types.js' const BUTTON_HEIGHT = '52px' -const BUTTON_HEIGHT_TEXT = '44px' -const BUTTON_HEIGHT_DESCRIPTIVE = '40px' +const BUTTON_HEIGHT_DESCRIPTIVE = '44px' +const GOOGLE_BUTTON_HEIGHT_DESCRIPTIVE = '40px' // Standard Google buttons have an intrinsic localized text width; narrow connector cells use the official icon variant. const GOOGLE_STANDARD_BUTTON_MIN_WIDTH = 240 const iconSizeClasses = 'w-8 h-8' -const iconTextSizeClasses = 'w-6 h-6' -const iconDescriptiveSizeClasses = 'w-5 h-5' +const iconDescriptiveSizeClasses = 'w-6 h-6' export const getLogo = (theme: any, walletProps: WalletProperties) => theme === 'dark' @@ -44,16 +43,13 @@ export const ConnectButton = (props: ConnectButtonProps) => { return ( onConnect(connector)} - style={{ - height: isDescriptive ? BUTTON_HEIGHT_DESCRIPTIVE : BUTTON_HEIGHT_TEXT, - ...(isDescriptive ? { borderRadius: '9999px', gap: '10px', padding: '0 16px' } : {}) - }} + style={{ height: BUTTON_HEIGHT_DESCRIPTIVE }} > - - + + {buttonCopy} @@ -196,7 +192,7 @@ export const GoogleWaasConnectButton = ( return () => mutationObserver.disconnect() }, [buttonWidth, theme, useIconButton]) - const buttonHeight = isDescriptive ? BUTTON_HEIGHT_DESCRIPTIVE : BUTTON_HEIGHT + const buttonHeight = isDescriptive ? GOOGLE_BUTTON_HEIGHT_DESCRIPTIVE : BUTTON_HEIGHT const useSequenceShell = isDescriptive && !useIconButton // GIS supports outline_dark, but @react-oauth/google's theme type has not caught up with the current API. const googleButtonTheme = (theme === 'dark' ? 'outline_dark' : 'outline') as GoogleLoginProps['theme'] From cd30023e10b2160885826bd308b56727eeb0c0fb Mon Sep 17 00:00:00 2001 From: Alexander Kolberg Date: Sat, 11 Jul 2026 21:46:06 +0300 Subject: [PATCH 4/7] docs: clarify Google login changeset --- .changeset/bright-mice-connect.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/bright-mice-connect.md b/.changeset/bright-mice-connect.md index fd175f7a9..2ad623685 100644 --- a/.changeset/bright-mice-connect.md +++ b/.changeset/bright-mice-connect.md @@ -2,4 +2,4 @@ '@0xsequence/connect': patch --- -Render the official Google WaaS sign-in button responsively, with a dark outlined theme and an icon fallback for compact layouts, so Google authentication remains visible and reliable without starting the connection loader before credentials are returned. +Fix Google WaaS sign-in clicks failing in Chromium by replacing the hidden, transformed iframe with a directly interactive Google button. From a2b740ab5e4ef746e069c6c5f3d7b6f057f8331b Mon Sep 17 00:00:00 2001 From: Alexander Kolberg Date: Sat, 11 Jul 2026 21:46:47 +0300 Subject: [PATCH 5/7] docs: identify Google WaaS v2 flow --- .changeset/bright-mice-connect.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/bright-mice-connect.md b/.changeset/bright-mice-connect.md index 2ad623685..a80448ff8 100644 --- a/.changeset/bright-mice-connect.md +++ b/.changeset/bright-mice-connect.md @@ -2,4 +2,4 @@ '@0xsequence/connect': patch --- -Fix Google WaaS sign-in clicks failing in Chromium by replacing the hidden, transformed iframe with a directly interactive Google button. +Fix Google WaaS v2 sign-in clicks failing in Chromium by replacing the hidden, transformed iframe with a directly interactive Google button. From bed0f49b5f069e0ff8d4b965fa3ae07d9cbf73d1 Mon Sep 17 00:00:00 2001 From: Tolgahan Date: Sun, 12 Jul 2026 15:08:24 +0300 Subject: [PATCH 6/7] fix(connect): refine Google WaaS sign-in button --- .gitignore | 1 + examples/react-waas/src/App.tsx | 25 +- .../react-waas/src/components/Homepage.tsx | 25 +- .../src/components/Connect/Connect.tsx | 107 +++++++-- .../ConnectButton/ConnectButton.tsx | 121 +++++----- .../GoogleSignInButton/GoogleSignInButton.tsx | 216 ++++++++++++++++++ .../src/components/SocialLink/SocialLink.tsx | 7 +- packages/connect/src/styles.ts | 4 + 8 files changed, 409 insertions(+), 97 deletions(-) create mode 100644 packages/connect/src/components/GoogleSignInButton/GoogleSignInButton.tsx diff --git a/.gitignore b/.gitignore index 059c54524..5012cd20e 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ dist/ .DS_Store .vscode .idea +.codex/ *.iml .cache package-lock.json diff --git a/examples/react-waas/src/App.tsx b/examples/react-waas/src/App.tsx index 4384c1515..95915b394 100644 --- a/examples/react-waas/src/App.tsx +++ b/examples/react-waas/src/App.tsx @@ -3,6 +3,7 @@ import { SequenceConnect } from '@0xsequence/connect' import { ThemeProvider } from '@0xsequence/design-system' import { SequenceWalletProvider } from '@0xsequence/wallet-widget' import { BrowserRouter, Route, Routes } from 'react-router-dom' +import { useMemo, useState } from 'react' import { Homepage } from './components/Homepage' import { InlineDemo } from './components/InlineDemo' @@ -10,14 +11,34 @@ import { XAuthCallback } from './components/XAuthCallback' import { checkoutConfig, config } from './config' export const App = () => { + const [useFullWidthSocials, setUseFullWidthSocials] = useState(config.connectConfig.signIn?.descriptiveSocials ?? false) + const demoConfig = useMemo( + () => ({ + ...config, + connectConfig: { + ...config.connectConfig, + signIn: { + ...config.connectConfig.signIn, + descriptiveSocials: useFullWidthSocials + } + } + }), + [useFullWidthSocials] + ) + return ( - + - } /> + + } + /> } /> } /> diff --git a/examples/react-waas/src/components/Homepage.tsx b/examples/react-waas/src/components/Homepage.tsx index 9847fcd7b..9608e8e28 100644 --- a/examples/react-waas/src/components/Homepage.tsx +++ b/examples/react-waas/src/components/Homepage.tsx @@ -1,11 +1,16 @@ import { useOpenConnectModal, useWallets } from '@0xsequence/connect' -import { Button, Image, useTheme } from '@0xsequence/design-system' +import { Button, Image, Switch, Text, useTheme } from '@0xsequence/design-system' import { Footer } from 'example-shared-components' import { Link } from 'react-router-dom' import { Connected } from './Connected' -export const Homepage = () => { +interface HomepageProps { + useFullWidthSocials: boolean + onUseFullWidthSocialsChange: (useFullWidthSocials: boolean) => void +} + +export const Homepage = ({ useFullWidthSocials, onUseFullWidthSocialsChange }: HomepageProps) => { const { wallets } = useWallets() const { setOpenConnectModal } = useOpenConnectModal() const { theme } = useTheme() @@ -33,6 +38,22 @@ export const Homepage = () => {
+ +
) : ( diff --git a/packages/connect/src/components/Connect/Connect.tsx b/packages/connect/src/components/Connect/Connect.tsx index 7ae9f815d..43cbe9c9a 100644 --- a/packages/connect/src/components/Connect/Connect.tsx +++ b/packages/connect/src/components/Connect/Connect.tsx @@ -588,13 +588,19 @@ export const Connect = (props: ConnectProps) => { const renderConnectorButton = ( connector: ExtendedConnector, - options?: { isDescriptive?: boolean; disableTooltip?: boolean } + options?: { + isDescriptive?: boolean + disableTooltip?: boolean + forceIcon?: boolean + googleButtonTheme?: 'filled_blue' | 'outline' + } ) => { const commonProps = { connector, onConnect, isDescriptive: options?.isDescriptive, - disableTooltip: options?.disableTooltip + disableTooltip: options?.disableTooltip, + forceIcon: options?.forceIcon } // Special handling for ecosystem connector - use config data for display @@ -651,7 +657,12 @@ export const Connect = (props: ConnectProps) => { ) case 'google-waas': return ( - + ) case 'apple-waas': return ( @@ -831,6 +842,7 @@ export const Connect = (props: ConnectProps) => { const showMoreSocialOptions = socialAuthConnectors.length > MAX_ITEM_PER_ROW const showMoreWalletOptions = walletConnectors.length > MAX_ITEM_PER_ROW + const googleSocialConnector = socialAuthConnectors.find(connector => connector._wallet?.id === 'google-waas') const socialConnectorsPerRow = showMoreSocialOptions && !descriptiveSocials ? MAX_ITEM_PER_ROW - 1 : socialAuthConnectors.length const walletConnectorsPerRow = showMoreWalletOptions ? MAX_ITEM_PER_ROW - 1 : walletConnectors.length @@ -1045,7 +1057,7 @@ export const Connect = (props: ConnectProps) => { )} {!hasPrimarySequenceConnection && ( -
+
<> {showEcosystemConnectorSection && ecosystemConnector && (
{
)} - {!hideSocialConnectOptions && showSocialConnectorSection && ( -
- {socialAuthConnectors.slice(0, socialConnectorsPerRow).map(connector => { - return ( -
- {renderConnectorButton(connector, { + {!hideSocialConnectOptions && + showSocialConnectorSection && + (() => { + const visibleSocialConnectors = socialAuthConnectors.slice(0, socialConnectorsPerRow) + const otherConnectors = visibleSocialConnectors.filter( + connector => connector._wallet?.id !== 'google-waas' + ) + const renderConnectorGroup = (connectors: ExtendedConnector[]) => ( +
+ {connectors.map(connector => { + const connectorButton = renderConnectorButton(connector, { isDescriptive: descriptiveSocials, - disableTooltip: config?.signIn?.disableTooltipForDescriptiveSocials - })} + disableTooltip: config?.signIn?.disableTooltipForDescriptiveSocials, + forceIcon: !descriptiveSocials && connector._wallet?.id === 'guest-waas' + }) + + return ( +
+ {connectorButton} +
+ ) + })} +
+ ) + if (!googleSocialConnector) { + return ( +
+ {renderConnectorGroup(visibleSocialConnectors)} + {showMoreSocialOptions && setShowExtendedList('social')} />}
) - })} - {showMoreSocialOptions && ( -
- setShowExtendedList('social')} /> + } + + if (descriptiveSocials) { + return ( +
+ +
+ {renderConnectorButton(googleSocialConnector, { + isDescriptive: true, + disableTooltip: true, + googleButtonTheme: 'filled_blue' + })} +
+ + {renderConnectorGroup(otherConnectors)} + {showMoreSocialOptions && setShowExtendedList('social')} />} +
+ ) + } + + return ( +
+ {renderConnectorGroup(otherConnectors)} + {showMoreSocialOptions && setShowExtendedList('social')} />} + + {renderConnectorButton(googleSocialConnector, { + isDescriptive: true, + disableTooltip: true, + googleButtonTheme: 'outline' + })}
- )} -
- )} + ) + })()} {!hideSocialConnectOptions && showSocialConnectorSection && showEmailInputSection && (
- + or diff --git a/packages/connect/src/components/ConnectButton/ConnectButton.tsx b/packages/connect/src/components/ConnectButton/ConnectButton.tsx index 750178eba..d0e937e29 100644 --- a/packages/connect/src/components/ConnectButton/ConnectButton.tsx +++ b/packages/connect/src/components/ConnectButton/ConnectButton.tsx @@ -1,5 +1,4 @@ import { Card, ContextMenuIcon, Text, Tooltip, useTheme } from '@0xsequence/design-system' -import { GoogleLogin, type GoogleLoginProps } from '@react-oauth/google' import { useEffect, useRef, useState } from 'react' import { appleAuthHelpers } from 'react-apple-signin-auth' @@ -7,6 +6,7 @@ import { getXIdToken } from '../../connectors/X/XAuth.js' import { LocalStorageKey } from '../../constants/localStorage.js' import { useStorage, useStorageItem } from '../../hooks/useStorage.js' import type { ExtendedConnector, WalletProperties } from '../../types.js' +import { GoogleSignInButton, type GoogleButtonTheme } from '../GoogleSignInButton/GoogleSignInButton.js' const BUTTON_HEIGHT = '52px' const BUTTON_HEIGHT_DESCRIPTIVE = '44px' @@ -27,14 +27,21 @@ interface ConnectButtonProps { onConnect: (connector: ExtendedConnector) => void isDescriptive?: boolean disableTooltip?: boolean + forceIcon?: boolean +} + +type GoogleWaasConnector = ExtendedConnector & { + params?: { + googleClientId?: string + } } export const ConnectButton = (props: ConnectButtonProps) => { - const { connector, label, disableTooltip, onConnect } = props + const { connector, label, disableTooltip, forceIcon = false, onConnect } = props const { theme } = useTheme() const walletProps = connector._wallet const isDescriptive = props.isDescriptive || false - const shouldRenderTextButton = isDescriptive || !!walletProps.ctaText + const shouldRenderTextButton = !forceIcon && (isDescriptive || !!walletProps.ctaText) const buttonCopy = walletProps.ctaText || `Continue with ${label || walletProps.name}`.trim() const Logo = getLogo(theme, walletProps) @@ -100,7 +107,7 @@ export const GuestWaasConnectButton = ( setConnectingConnector?: (connector: ExtendedConnector | null) => void } ) => { - const { connector, onConnect, setIsLoading, setConnectingConnector } = props + const { connector, onConnect, forceIcon, setIsLoading, setConnectingConnector } = props return ( ) } export const GoogleWaasConnectButton = ( props: ConnectButtonProps & { + buttonTheme?: 'filled_blue' | 'outline' setIsLoading?: (isLoading: boolean) => void setConnectingConnector?: (connector: ExtendedConnector | null) => void } ) => { - const { connector, onConnect, isDescriptive = false, setIsLoading, setConnectingConnector } = props + const { connector, onConnect, isDescriptive = false, buttonTheme = 'outline', setIsLoading, setConnectingConnector } = props const storage = useStorage() const containerRef = useRef(null) - const googleButtonRef = useRef(null) const isMountedRef = useRef(true) const [buttonWidth, setButtonWidth] = useState(0) - const [useIconButton, setUseIconButton] = useState(true) const { theme } = useTheme() + const googleClientId = (connector as GoogleWaasConnector).params?.googleClientId ?? '' useEffect(() => { isMountedRef.current = true @@ -140,83 +147,68 @@ export const GoogleWaasConnectButton = ( }, []) useEffect(() => { - const updateButtonWidth = () => { - const availableWidth = containerRef.current?.clientWidth ?? 0 - const nextButtonWidth = Math.min(400, Math.floor(availableWidth)) - setButtonWidth(currentWidth => (currentWidth === nextButtonWidth ? currentWidth : nextButtonWidth)) - } - - updateButtonWidth() - - if (typeof ResizeObserver === 'undefined') { - window.addEventListener('resize', updateButtonWidth) - return () => window.removeEventListener('resize', updateButtonWidth) - } + let hasMeasured = false + let resizeObserver: ResizeObserver | undefined - const resizeObserver = new ResizeObserver(updateButtonWidth) - if (containerRef.current) { - resizeObserver.observe(containerRef.current) - } - - return () => resizeObserver.disconnect() - }, []) + const measureInitialWidth = () => { + if (hasMeasured) { + return + } - useEffect(() => { - setUseIconButton(buttonWidth < GOOGLE_STANDARD_BUTTON_MIN_WIDTH) - }, [buttonWidth, theme]) + const availableWidth = containerRef.current?.clientWidth ?? 0 + if (availableWidth === 0) { + return + } - useEffect(() => { - if (useIconButton) { - return + hasMeasured = true + const nextButtonWidth = Math.min(400, Math.floor(availableWidth)) + setButtonWidth(nextButtonWidth) + resizeObserver?.disconnect() + window.removeEventListener('resize', measureInitialWidth) } - const buttonContainer = googleButtonRef.current - const availableWidth = containerRef.current?.clientWidth ?? 0 - if (!buttonContainer || availableWidth === 0) { - return - } + measureInitialWidth() - const checkButtonOverflow = () => { - // GIS makes its iframe wider than the visible button to add click padding, so measure Google's immediate wrapper instead. - const renderedButton = buttonContainer.querySelector('iframe')?.parentElement - if (renderedButton && renderedButton.getBoundingClientRect().width > availableWidth + 1) { - setUseIconButton(true) + if (!hasMeasured) { + if (typeof ResizeObserver === 'undefined') { + window.addEventListener('resize', measureInitialWidth) + } else { + resizeObserver = new ResizeObserver(measureInitialWidth) + if (containerRef.current) { + resizeObserver.observe(containerRef.current) + } } } - checkButtonOverflow() - - const mutationObserver = new MutationObserver(checkButtonOverflow) - mutationObserver.observe(buttonContainer, { childList: true, subtree: true, attributes: true }) - - return () => mutationObserver.disconnect() - }, [buttonWidth, theme, useIconButton]) + return () => { + resizeObserver?.disconnect() + window.removeEventListener('resize', measureInitialWidth) + } + }, []) + const useIconButton = buttonWidth < GOOGLE_STANDARD_BUTTON_MIN_WIDTH const buttonHeight = isDescriptive ? GOOGLE_BUTTON_HEIGHT_DESCRIPTIVE : BUTTON_HEIGHT - const useSequenceShell = isDescriptive && !useIconButton // GIS supports outline_dark, but @react-oauth/google's theme type has not caught up with the current API. - const googleButtonTheme = (theme === 'dark' ? 'outline_dark' : 'outline') as GoogleLoginProps['theme'] + const googleButtonTheme: GoogleButtonTheme = + buttonTheme === 'filled_blue' ? 'filled_blue' : theme === 'dark' ? 'outline_dark' : 'outline' return (
-
+
{buttonWidth > 0 && ( - { // GIS may finish after the modal has unmounted; ignore stale callbacks after dismissal. if (!isMountedRef.current) { @@ -240,15 +232,6 @@ export const GoogleWaasConnectButton = ( /> )}
- {useSequenceShell && ( - // Keep Google's iframe directly interactive while replacing only its outer border with Sequence's visual shell. -
-
-
- )}
) } diff --git a/packages/connect/src/components/GoogleSignInButton/GoogleSignInButton.tsx b/packages/connect/src/components/GoogleSignInButton/GoogleSignInButton.tsx new file mode 100644 index 000000000..e593eb6b1 --- /dev/null +++ b/packages/connect/src/components/GoogleSignInButton/GoogleSignInButton.tsx @@ -0,0 +1,216 @@ +import { Skeleton } from '@0xsequence/design-system' +import type { CredentialResponse, GsiButtonConfiguration, IdConfiguration } from '@react-oauth/google' +import { useEffect, useId, useRef, useState } from 'react' + +export type GoogleButtonTheme = 'outline' | 'outline_dark' | 'filled_blue' | 'filled_black' + +interface RoutedCredentialResponse extends CredentialResponse { + state?: string +} + +interface GoogleButtonConfiguration extends Omit { + state?: string + theme?: GoogleButtonTheme +} + +interface GoogleIdentityApi { + initialize: (config: IdConfiguration) => void + renderButton: (parent: HTMLElement, options: GoogleButtonConfiguration) => void +} + +interface GoogleSignInButtonProps extends Omit { + clientId: string + theme?: GoogleButtonTheme + onSuccess: (credentialResponse: CredentialResponse) => void + onError?: () => void +} + +const buttonHeights = { + large: 40, + medium: 32, + small: 20 +} as const +const MIN_LOADING_DURATION_MS = 600 + +type CredentialHandler = (credentialResponse: CredentialResponse) => void + +let initializedGoogleIdentity: GoogleIdentityApi | undefined +let initializedGoogleClientId: string | undefined +const credentialHandlers = new Map() + +const getGoogleIdentity = (): GoogleIdentityApi | undefined => (window as any).google?.accounts?.id + +const initializeGoogleIdentity = (googleIdentity: GoogleIdentityApi, clientId: string) => { + if (initializedGoogleIdentity === googleIdentity) { + if (initializedGoogleClientId !== clientId) { + console.error('Google Identity Services cannot be initialized with multiple client IDs on the same page.') + return false + } + return true + } + + googleIdentity.initialize({ + client_id: clientId, + callback: credentialResponse => { + const buttonState = (credentialResponse as RoutedCredentialResponse).state + const credentialHandler = buttonState ? credentialHandlers.get(buttonState) : undefined + + if (!credentialHandler) { + console.error('Unable to match the Google credential response to the button that initiated it.') + return + } + + credentialHandler(credentialResponse) + } + }) + initializedGoogleIdentity = googleIdentity + initializedGoogleClientId = clientId + return true +} + +export const GoogleSignInButton = ({ + clientId, + onSuccess, + onError, + type = 'standard', + theme = 'outline', + size = 'large', + text, + shape, + logo_alignment, + width, + locale, + click_listener +}: GoogleSignInButtonProps) => { + const containerRef = useRef(null) + const onSuccessRef = useRef(onSuccess) + const onErrorRef = useRef(onError) + const buttonState = `sequence-google-${useId().replace(/[^a-zA-Z0-9_-]/g, '')}` + const renderSignature = [clientId, type, theme, size, text, shape, logo_alignment, width, locale].join('|') + const [readySignature, setReadySignature] = useState() + const isReady = readySignature === renderSignature + const buttonHeight = buttonHeights[size] + const containerWidth = + type === 'icon' ? buttonHeight : typeof width === 'string' && /^\d+$/.test(width) ? Number(width) : (width ?? '100%') + + onSuccessRef.current = onSuccess + onErrorRef.current = onError + + useEffect(() => { + if (!clientId) { + return + } + + let isCancelled = false + let retryTimer: ReturnType | undefined + let revealTimer: ReturnType | undefined + let renderedIframe: HTMLIFrameElement | undefined + const loadingStartedAt = performance.now() + + const handleCredential = (credentialResponse: CredentialResponse) => { + if (credentialResponse.credential) { + onSuccessRef.current(credentialResponse) + } else { + onErrorRef.current?.() + } + } + + const handleIframeLoad = () => { + if (!isCancelled && renderedIframe?.src.includes('accounts.google.com/gsi/button')) { + const remainingLoadingTime = Math.max(0, MIN_LOADING_DURATION_MS - (performance.now() - loadingStartedAt)) + revealTimer = setTimeout(() => setReadySignature(renderSignature), remainingLoadingTime) + } + } + + const findRenderedIframe = () => { + const nextIframe = containerRef.current?.querySelector('iframe[src*="accounts.google.com/gsi/button"]') + if (!nextIframe || nextIframe === renderedIframe) { + return + } + + renderedIframe?.removeEventListener('load', handleIframeLoad) + renderedIframe = nextIframe + renderedIframe.addEventListener('load', handleIframeLoad, { once: true }) + } + + const mutationObserver = new MutationObserver(findRenderedIframe) + + const renderButton = () => { + if (isCancelled) { + return + } + + const googleIdentity = getGoogleIdentity() + const container = containerRef.current + if (!googleIdentity || !container) { + retryTimer = setTimeout(renderButton, 50) + return + } + + credentialHandlers.set(buttonState, handleCredential) + if (!initializeGoogleIdentity(googleIdentity, clientId)) { + credentialHandlers.delete(buttonState) + onErrorRef.current?.() + return + } + + mutationObserver.observe(container, { childList: true, subtree: true }) + container.replaceChildren() + googleIdentity.renderButton(container, { + type, + theme, + size, + text, + shape, + logo_alignment, + width, + locale, + click_listener, + state: buttonState + }) + findRenderedIframe() + } + + renderButton() + + return () => { + isCancelled = true + if (retryTimer) { + clearTimeout(retryTimer) + } + if (revealTimer) { + clearTimeout(revealTimer) + } + mutationObserver.disconnect() + renderedIframe?.removeEventListener('load', handleIframeLoad) + if (credentialHandlers.get(buttonState) === handleCredential) { + credentialHandlers.delete(buttonState) + } + } + }, [buttonState, clientId, type, theme, size, text, shape, logo_alignment, width, locale, click_listener, renderSignature]) + + return ( +
+
+ +
+
+ {!isReady && ( + + Loading Google sign-in + + )} +
+ ) +} diff --git a/packages/connect/src/components/SocialLink/SocialLink.tsx b/packages/connect/src/components/SocialLink/SocialLink.tsx index f676c391d..7e5ff8c5b 100644 --- a/packages/connect/src/components/SocialLink/SocialLink.tsx +++ b/packages/connect/src/components/SocialLink/SocialLink.tsx @@ -1,6 +1,6 @@ import { Button, Card, PINCodeInput, Separator, Spinner, Text, TextInput } from '@0xsequence/design-system' import { type Account } from '@0xsequence/waas' -import { GoogleLogin, type CredentialResponse } from '@react-oauth/google' +import { type CredentialResponse } from '@react-oauth/google' import { useEffect, useRef, useState, type SetStateAction } from 'react' import AppleSignin from 'react-apple-signin-auth' import { english } from 'viem/accounts' @@ -9,6 +9,7 @@ import { LocalStorageKey } from '../../constants/localStorage.js' import { useSequenceWaaS } from '../../hooks/useSequenceWaaS.js' import { useStorageItem } from '../../hooks/useStorage.js' import { isAccountAlreadyLinkedError, useEmailAuth } from '../../utils/useEmailAuth.js' +import { GoogleSignInButton } from '../GoogleSignInButton/GoogleSignInButton.js' import { AccountName } from './AccountName.js' @@ -168,7 +169,9 @@ export function SocialLink() {
- {googleClientId && } + {googleClientId && ( + + )} {appleClientId && ( // @ts-ignore Date: Sun, 12 Jul 2026 21:52:24 +0300 Subject: [PATCH 7/7] refactor(connect): remove stale Google loading state --- packages/connect/src/components/Connect/Connect.tsx | 9 +-------- .../src/components/ConnectButton/ConnectButton.tsx | 6 +----- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/packages/connect/src/components/Connect/Connect.tsx b/packages/connect/src/components/Connect/Connect.tsx index 43cbe9c9a..44e9ee639 100644 --- a/packages/connect/src/components/Connect/Connect.tsx +++ b/packages/connect/src/components/Connect/Connect.tsx @@ -656,14 +656,7 @@ export const Connect = (props: ConnectProps) => { ) case 'google-waas': - return ( - - ) + return case 'apple-waas': return ( diff --git a/packages/connect/src/components/ConnectButton/ConnectButton.tsx b/packages/connect/src/components/ConnectButton/ConnectButton.tsx index d0e937e29..c4a51af43 100644 --- a/packages/connect/src/components/ConnectButton/ConnectButton.tsx +++ b/packages/connect/src/components/ConnectButton/ConnectButton.tsx @@ -126,11 +126,9 @@ export const GuestWaasConnectButton = ( export const GoogleWaasConnectButton = ( props: ConnectButtonProps & { buttonTheme?: 'filled_blue' | 'outline' - setIsLoading?: (isLoading: boolean) => void - setConnectingConnector?: (connector: ExtendedConnector | null) => void } ) => { - const { connector, onConnect, isDescriptive = false, buttonTheme = 'outline', setIsLoading, setConnectingConnector } = props + const { connector, onConnect, isDescriptive = false, buttonTheme = 'outline' } = props const storage = useStorage() const containerRef = useRef(null) const isMountedRef = useRef(true) @@ -226,8 +224,6 @@ export const GoogleWaasConnectButton = ( } console.log('Login Failed') - setIsLoading?.(false) - setConnectingConnector?.(null) }} /> )}