From baaeaf8b05112cb8594e5f8c706bc690350563d9 Mon Sep 17 00:00:00 2001 From: ragnep Date: Tue, 24 Mar 2026 11:15:29 +0200 Subject: [PATCH 1/5] wip Signed-off-by: ragnep --- components/auth/Auth.tsx | 37 +++++++++++++ components/auth/SeizeConnectContext.tsx | 38 +++++++++++++ components/waves/WaveScreenMessage.tsx | 27 +++++++++ components/waves/WavesView.tsx | 47 ++++++++-------- components/waves/layout/WavesLayout.tsx | 42 ++++++-------- components/waves/public/PublicWaveShell.tsx | 18 ++++-- .../waves/public/usePublicWaveShellState.ts | 2 +- helpers/waves-auth-return.helpers.ts | 55 +++++++++++++++++++ 8 files changed, 210 insertions(+), 56 deletions(-) create mode 100644 components/waves/WaveScreenMessage.tsx create mode 100644 helpers/waves-auth-return.helpers.ts diff --git a/components/auth/Auth.tsx b/components/auth/Auth.tsx index 0d252c5004..22a81f855d 100644 --- a/components/auth/Auth.tsx +++ b/components/auth/Auth.tsx @@ -27,6 +27,11 @@ import type { ApiNonceResponse } from "@/generated/models/ApiNonceResponse"; import type { ApiProfileProxy } from "@/generated/models/ApiProfileProxy"; import { getActiveWaveIdFromUrl } from "@/helpers/navigation.helpers"; import { groupProfileProxies } from "@/helpers/profile-proxy.helpers"; +import { + clearPendingWavesAuthReturnUrl, + getCurrentBrowserRelativeUrl, + getPendingWavesAuthReturnUrl, +} from "@/helpers/waves-auth-return.helpers"; import { getProfileConnectedStatus } from "@/helpers/ProfileHelpers"; import { useIdentity } from "@/hooks/useIdentity"; import { @@ -806,6 +811,38 @@ export default function Auth({ const showWaves = useMemo(() => { return !!connectedProfile?.handle && !activeProfileProxy && !!address; }, [connectedProfile?.handle, activeProfileProxy, address]); + const previousShowWavesRef = useRef(showWaves); + + // The pending `/waves` return target can only be restored after async auth + // hydration has made waves available again, so this has to observe auth state. + /* eslint-disable react-you-might-not-need-an-effect/no-event-handler */ + useEffect(() => { + const didBecomeReady = !previousShowWavesRef.current && showWaves; + previousShowWavesRef.current = showWaves; + + if (!showWaves) { + return; + } + + const pendingWavesReturnUrl = getPendingWavesAuthReturnUrl(); + if (!pendingWavesReturnUrl) { + return; + } + + const currentRelativeUrl = getCurrentBrowserRelativeUrl(); + if (currentRelativeUrl === pendingWavesReturnUrl) { + clearPendingWavesAuthReturnUrl(); + return; + } + + if (!didBecomeReady) { + return; + } + + clearPendingWavesAuthReturnUrl(); + router.replace(pendingWavesReturnUrl); + }, [showWaves, router]); + /* eslint-enable react-you-might-not-need-an-effect/no-event-handler */ const onCancelSignRequest = useCallback(() => { setShowSignModal(false); diff --git a/components/auth/SeizeConnectContext.tsx b/components/auth/SeizeConnectContext.tsx index 6fb45c0f34..0ab1e71ef1 100644 --- a/components/auth/SeizeConnectContext.tsx +++ b/components/auth/SeizeConnectContext.tsx @@ -19,6 +19,11 @@ import React, { import { getAddress, isAddress } from "viem"; import { getNodeEnv, publicEnv } from "@/config/env"; import { MAX_CONNECTED_PROFILES } from "@/constants/constants"; +import { + clearPendingWavesAuthReturnUrl, + getPendingWavesAuthReturnUrl, + preparePendingWavesAuthReturnUrlForCurrentLocation, +} from "@/helpers/waves-auth-return.helpers"; import { canStoreAnotherWalletAccount, type ConnectedWalletAccount, @@ -206,6 +211,7 @@ const isCapacitorPlatform = (): boolean => { const normalizeAddress = (address: string): string => address.toLowerCase(); const ADD_FLOW_CANCEL_GRACE_MS: number = 5000; +const WAVES_AUTH_RETURN_CLEAR_GRACE_MS: number = 5000; const validateStoredAddress = ( storedAddress: string @@ -741,6 +747,34 @@ export const SeizeConnectProvider: React.FC<{ children: React.ReactNode }> = ({ state.open, ]); + useEffect(() => { + const pendingWavesReturnUrl = getPendingWavesAuthReturnUrl(); + if (!pendingWavesReturnUrl) { + return; + } + + const hasLiveProviderConnection = Boolean( + account.address && account.isConnected && isAddress(account.address) + ); + const isConnectFlowActive = + state.open || + account.status === "connecting" || + account.status === "reconnecting" || + hasLiveProviderConnection; + + if (isConnectFlowActive) { + return; + } + + const clearTimer = setTimeout(() => { + clearPendingWavesAuthReturnUrl(); + }, WAVES_AUTH_RETURN_CLEAR_GRACE_MS); + + return () => { + clearTimeout(clearTimer); + }; + }, [account.address, account.isConnected, account.status, state.open]); + const activeAddress = impersonatedAddress ?? connectedAddress; const liveConnectedAddress = impersonatedAddress || @@ -755,6 +789,8 @@ export const SeizeConnectProvider: React.FC<{ children: React.ReactNode }> = ({ const seizeConnect = useCallback((): void => { try { + preparePendingWavesAuthReturnUrlForCurrentLocation(); + // Log connection attempt for security monitoring logSecurityEvent( SecurityEventType.WALLET_CONNECTION_ATTEMPT, @@ -769,6 +805,8 @@ export const SeizeConnectProvider: React.FC<{ children: React.ReactNode }> = ({ createConnectionEventContext("seizeConnect") ); } catch (error) { + clearPendingWavesAuthReturnUrl(); + const connectionError = new WalletConnectionError( "Failed to open wallet connection modal", error diff --git a/components/waves/WaveScreenMessage.tsx b/components/waves/WaveScreenMessage.tsx new file mode 100644 index 0000000000..c6d64dad2d --- /dev/null +++ b/components/waves/WaveScreenMessage.tsx @@ -0,0 +1,27 @@ +import type { ReactNode } from "react"; + +interface WaveScreenMessageProps { + readonly title: ReactNode; + readonly description: ReactNode; + readonly action?: ReactNode; +} + +export default function WaveScreenMessage({ + title, + description, + action, +}: WaveScreenMessageProps) { + return ( +
+

+ {title} +

+

+ {description} +

+ {action !== undefined && action !== null ? ( +
{action}
+ ) : null} +
+ ); +} diff --git a/components/waves/WavesView.tsx b/components/waves/WavesView.tsx index 2a81c130c8..4353a5ae8f 100644 --- a/components/waves/WavesView.tsx +++ b/components/waves/WavesView.tsx @@ -9,6 +9,7 @@ import useDeviceInfo from "../../hooks/useDeviceInfo"; import PrimaryButton from "../utils/button/PrimaryButton"; import useCreateModalState from "@/hooks/useCreateModalState"; import { useMyStreamOptional } from "@/contexts/wave/MyStreamContext"; +import WaveScreenMessage from "./WaveScreenMessage"; const WavesView: React.FC = () => { const myStream = useMyStreamOptional(); @@ -22,32 +23,31 @@ const WavesView: React.FC = () => { let content: React.ReactNode = null; - if (serialisedWaveId) { content = ( - + ); } else if (showPlaceholder) { content = ( -
-

- Select a Wave -

-

- Choose a wave to view its content and participate in the discussion. -

- - {connectedProfile && ( - - - Create Wave - - )} -
+ + + Create Wave + + ) : null + } + /> ); } @@ -55,12 +55,9 @@ const WavesView: React.FC = () => { // internally via MyStreamWaveChat. We pass null to BrainContent because // the wave's internal state controls the reply/quote input box. return ( - { }}> + {}}> {content} - ); }; diff --git a/components/waves/layout/WavesLayout.tsx b/components/waves/layout/WavesLayout.tsx index ce3fbbc313..bcc5443356 100644 --- a/components/waves/layout/WavesLayout.tsx +++ b/components/waves/layout/WavesLayout.tsx @@ -6,14 +6,12 @@ import { getActiveWaveIdFromUrl } from "@/helpers/navigation.helpers"; import { useAuthenticatedContent } from "../../../hooks/useAuthenticatedContent"; import useDeviceInfo from "../../../hooks/useDeviceInfo"; import ConnectWallet from "../../common/ConnectWallet"; +import HeaderUserConnect from "../../header/user/HeaderUserConnect"; import UserSetUpProfileCta from "../../user/utils/set-up-profile/UserSetUpProfileCta"; import WavesDesktop from "../WavesDesktop"; import WavesMobile from "../WavesMobile"; import PublicWaveShell from "../public/PublicWaveShell"; -import { - type PublicWaveShellState, - usePublicWaveShellState, -} from "../public/usePublicWaveShellState"; +import WaveScreenMessage from "../WaveScreenMessage"; function getConnectPrompt( contentState: ReturnType["contentState"] @@ -47,27 +45,17 @@ function getNotAuthenticatedContent({ activeWaveId, containerClassName, isApp, - publicWaveShellState, + isMobileDevice, }: { readonly activeWaveId: string | null; readonly containerClassName: string; readonly isApp: boolean; - readonly publicWaveShellState: PublicWaveShellState; + readonly isMobileDevice: boolean; }): ReactNode { - if (isApp) { + if (isApp || (isMobileDevice && activeWaveId === null)) { return ; } - if (activeWaveId === null || publicWaveShellState.status === "unavailable") { - return ; - } - - const publicShell = ( -
- -
- ); - return (
- {publicShell} +
+ {activeWaveId === null ? ( + } + /> + ) : ( + + )} +
); @@ -84,14 +82,10 @@ function getNotAuthenticatedContent({ // Main layout content that uses the Layout context function WavesLayoutContent({ children }: { readonly children: ReactNode }) { const { contentState } = useAuthenticatedContent(); - const { isApp } = useDeviceInfo(); + const { isApp, isMobileDevice } = useDeviceInfo(); const pathname = usePathname(); const searchParams = useSearchParams(); const activeWaveId = getActiveWaveIdFromUrl({ pathname, searchParams }); - const publicWaveShellState = usePublicWaveShellState(activeWaveId, { - enabled: - !isApp && contentState === "not-authenticated" && activeWaveId !== null, - }); const containerClassName = "tw-relative tw-flex tw-flex-col tw-flex-1 tailwind-scope"; @@ -113,7 +107,7 @@ function WavesLayoutContent({ children }: { readonly children: ReactNode }) { activeWaveId, containerClassName, isApp, - publicWaveShellState, + isMobileDevice, }); } else { content = diff --git a/components/waves/public/PublicWaveShell.tsx b/components/waves/public/PublicWaveShell.tsx index 08c0c63865..5d3aafd5e1 100644 --- a/components/waves/public/PublicWaveShell.tsx +++ b/components/waves/public/PublicWaveShell.tsx @@ -1,11 +1,11 @@ "use client"; -import ConnectWallet from "@/components/common/ConnectWallet"; import SpinnerLoader from "@/components/common/SpinnerLoader"; import HeaderUserConnect from "@/components/header/user/HeaderUserConnect"; import WavePicture from "@/components/waves/WavePicture"; import { formatCount } from "@/helpers/format.helpers"; import { LockClosedIcon } from "@heroicons/react/24/outline"; +import WaveScreenMessage from "../WaveScreenMessage"; import LoggedOutSkeleton from "./LoggedOutSkeleton"; import { type PublicWaveShellData, @@ -34,13 +34,19 @@ function compactDescriptionText(text: string): string { function PublicWaveLoadingState() { return (
- +
); } function PublicWaveUnavailableState() { - return ; + return ( + } + /> + ); } function PublicWaveShellContent({ @@ -73,19 +79,19 @@ function PublicWaveShellContent({
-
+
-

+

{wave.name}

{hasDescription && ( -

+

{description}

)} diff --git a/components/waves/public/usePublicWaveShellState.ts b/components/waves/public/usePublicWaveShellState.ts index 508b82c656..de74e6d392 100644 --- a/components/waves/public/usePublicWaveShellState.ts +++ b/components/waves/public/usePublicWaveShellState.ts @@ -13,7 +13,7 @@ export interface PublicWaveShellData { readonly postsCount: number; } -export type PublicWaveShellState = +type PublicWaveShellState = | { readonly status: "loading" } | { readonly status: "unavailable" } | { readonly status: "ready"; readonly wave: PublicWaveShellData }; diff --git a/helpers/waves-auth-return.helpers.ts b/helpers/waves-auth-return.helpers.ts new file mode 100644 index 0000000000..dd9aeef8d2 --- /dev/null +++ b/helpers/waves-auth-return.helpers.ts @@ -0,0 +1,55 @@ +import { safeSessionStorage } from "@/helpers/safeSessionStorage"; + +const WAVES_AUTH_RETURN_URL_KEY = "6529-waves-auth-return-url"; +const URL_PARSE_BASE = "https://6529.io"; + +function isWavesPathname(pathname: string): boolean { + return pathname === "/waves" || pathname.startsWith("/waves/"); +} + +function normalizeWavesAuthReturnUrl(value: string | null): string | null { + if (!value) { + return null; + } + + try { + const parsed = new URL(value, URL_PARSE_BASE); + if (!isWavesPathname(parsed.pathname)) { + return null; + } + + return `${parsed.pathname}${parsed.search}${parsed.hash}`; + } catch { + return null; + } +} + +export function getCurrentBrowserRelativeUrl(): string | null { + if (typeof window === "undefined") { + return null; + } + + return `${window.location.pathname}${window.location.search}${window.location.hash}`; +} + +export function getPendingWavesAuthReturnUrl(): string | null { + return normalizeWavesAuthReturnUrl( + safeSessionStorage.getItem(WAVES_AUTH_RETURN_URL_KEY) + ); +} + +export function clearPendingWavesAuthReturnUrl(): void { + safeSessionStorage.removeItem(WAVES_AUTH_RETURN_URL_KEY); +} + +export function preparePendingWavesAuthReturnUrlForCurrentLocation(): void { + const currentRelativeUrl = getCurrentBrowserRelativeUrl(); + const normalizedReturnUrl = normalizeWavesAuthReturnUrl(currentRelativeUrl); + + if (!normalizedReturnUrl) { + clearPendingWavesAuthReturnUrl(); + return; + } + + safeSessionStorage.setItem(WAVES_AUTH_RETURN_URL_KEY, normalizedReturnUrl); +} From 0e31fa4ed917dd59f18b1a38d3be7a7aca095367 Mon Sep 17 00:00:00 2001 From: ragnep Date: Tue, 24 Mar 2026 11:51:11 +0200 Subject: [PATCH 2/5] wip Signed-off-by: ragnep --- components/auth/Auth.tsx | 37 ----------------- components/auth/SeizeConnectContext.tsx | 38 ----------------- helpers/waves-auth-return.helpers.ts | 55 ------------------------- 3 files changed, 130 deletions(-) delete mode 100644 helpers/waves-auth-return.helpers.ts diff --git a/components/auth/Auth.tsx b/components/auth/Auth.tsx index 22a81f855d..0d252c5004 100644 --- a/components/auth/Auth.tsx +++ b/components/auth/Auth.tsx @@ -27,11 +27,6 @@ import type { ApiNonceResponse } from "@/generated/models/ApiNonceResponse"; import type { ApiProfileProxy } from "@/generated/models/ApiProfileProxy"; import { getActiveWaveIdFromUrl } from "@/helpers/navigation.helpers"; import { groupProfileProxies } from "@/helpers/profile-proxy.helpers"; -import { - clearPendingWavesAuthReturnUrl, - getCurrentBrowserRelativeUrl, - getPendingWavesAuthReturnUrl, -} from "@/helpers/waves-auth-return.helpers"; import { getProfileConnectedStatus } from "@/helpers/ProfileHelpers"; import { useIdentity } from "@/hooks/useIdentity"; import { @@ -811,38 +806,6 @@ export default function Auth({ const showWaves = useMemo(() => { return !!connectedProfile?.handle && !activeProfileProxy && !!address; }, [connectedProfile?.handle, activeProfileProxy, address]); - const previousShowWavesRef = useRef(showWaves); - - // The pending `/waves` return target can only be restored after async auth - // hydration has made waves available again, so this has to observe auth state. - /* eslint-disable react-you-might-not-need-an-effect/no-event-handler */ - useEffect(() => { - const didBecomeReady = !previousShowWavesRef.current && showWaves; - previousShowWavesRef.current = showWaves; - - if (!showWaves) { - return; - } - - const pendingWavesReturnUrl = getPendingWavesAuthReturnUrl(); - if (!pendingWavesReturnUrl) { - return; - } - - const currentRelativeUrl = getCurrentBrowserRelativeUrl(); - if (currentRelativeUrl === pendingWavesReturnUrl) { - clearPendingWavesAuthReturnUrl(); - return; - } - - if (!didBecomeReady) { - return; - } - - clearPendingWavesAuthReturnUrl(); - router.replace(pendingWavesReturnUrl); - }, [showWaves, router]); - /* eslint-enable react-you-might-not-need-an-effect/no-event-handler */ const onCancelSignRequest = useCallback(() => { setShowSignModal(false); diff --git a/components/auth/SeizeConnectContext.tsx b/components/auth/SeizeConnectContext.tsx index 0ab1e71ef1..6fb45c0f34 100644 --- a/components/auth/SeizeConnectContext.tsx +++ b/components/auth/SeizeConnectContext.tsx @@ -19,11 +19,6 @@ import React, { import { getAddress, isAddress } from "viem"; import { getNodeEnv, publicEnv } from "@/config/env"; import { MAX_CONNECTED_PROFILES } from "@/constants/constants"; -import { - clearPendingWavesAuthReturnUrl, - getPendingWavesAuthReturnUrl, - preparePendingWavesAuthReturnUrlForCurrentLocation, -} from "@/helpers/waves-auth-return.helpers"; import { canStoreAnotherWalletAccount, type ConnectedWalletAccount, @@ -211,7 +206,6 @@ const isCapacitorPlatform = (): boolean => { const normalizeAddress = (address: string): string => address.toLowerCase(); const ADD_FLOW_CANCEL_GRACE_MS: number = 5000; -const WAVES_AUTH_RETURN_CLEAR_GRACE_MS: number = 5000; const validateStoredAddress = ( storedAddress: string @@ -747,34 +741,6 @@ export const SeizeConnectProvider: React.FC<{ children: React.ReactNode }> = ({ state.open, ]); - useEffect(() => { - const pendingWavesReturnUrl = getPendingWavesAuthReturnUrl(); - if (!pendingWavesReturnUrl) { - return; - } - - const hasLiveProviderConnection = Boolean( - account.address && account.isConnected && isAddress(account.address) - ); - const isConnectFlowActive = - state.open || - account.status === "connecting" || - account.status === "reconnecting" || - hasLiveProviderConnection; - - if (isConnectFlowActive) { - return; - } - - const clearTimer = setTimeout(() => { - clearPendingWavesAuthReturnUrl(); - }, WAVES_AUTH_RETURN_CLEAR_GRACE_MS); - - return () => { - clearTimeout(clearTimer); - }; - }, [account.address, account.isConnected, account.status, state.open]); - const activeAddress = impersonatedAddress ?? connectedAddress; const liveConnectedAddress = impersonatedAddress || @@ -789,8 +755,6 @@ export const SeizeConnectProvider: React.FC<{ children: React.ReactNode }> = ({ const seizeConnect = useCallback((): void => { try { - preparePendingWavesAuthReturnUrlForCurrentLocation(); - // Log connection attempt for security monitoring logSecurityEvent( SecurityEventType.WALLET_CONNECTION_ATTEMPT, @@ -805,8 +769,6 @@ export const SeizeConnectProvider: React.FC<{ children: React.ReactNode }> = ({ createConnectionEventContext("seizeConnect") ); } catch (error) { - clearPendingWavesAuthReturnUrl(); - const connectionError = new WalletConnectionError( "Failed to open wallet connection modal", error diff --git a/helpers/waves-auth-return.helpers.ts b/helpers/waves-auth-return.helpers.ts deleted file mode 100644 index dd9aeef8d2..0000000000 --- a/helpers/waves-auth-return.helpers.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { safeSessionStorage } from "@/helpers/safeSessionStorage"; - -const WAVES_AUTH_RETURN_URL_KEY = "6529-waves-auth-return-url"; -const URL_PARSE_BASE = "https://6529.io"; - -function isWavesPathname(pathname: string): boolean { - return pathname === "/waves" || pathname.startsWith("/waves/"); -} - -function normalizeWavesAuthReturnUrl(value: string | null): string | null { - if (!value) { - return null; - } - - try { - const parsed = new URL(value, URL_PARSE_BASE); - if (!isWavesPathname(parsed.pathname)) { - return null; - } - - return `${parsed.pathname}${parsed.search}${parsed.hash}`; - } catch { - return null; - } -} - -export function getCurrentBrowserRelativeUrl(): string | null { - if (typeof window === "undefined") { - return null; - } - - return `${window.location.pathname}${window.location.search}${window.location.hash}`; -} - -export function getPendingWavesAuthReturnUrl(): string | null { - return normalizeWavesAuthReturnUrl( - safeSessionStorage.getItem(WAVES_AUTH_RETURN_URL_KEY) - ); -} - -export function clearPendingWavesAuthReturnUrl(): void { - safeSessionStorage.removeItem(WAVES_AUTH_RETURN_URL_KEY); -} - -export function preparePendingWavesAuthReturnUrlForCurrentLocation(): void { - const currentRelativeUrl = getCurrentBrowserRelativeUrl(); - const normalizedReturnUrl = normalizeWavesAuthReturnUrl(currentRelativeUrl); - - if (!normalizedReturnUrl) { - clearPendingWavesAuthReturnUrl(); - return; - } - - safeSessionStorage.setItem(WAVES_AUTH_RETURN_URL_KEY, normalizedReturnUrl); -} From 227477d27bd81403ef7ff4e03e04e4ed8fc3876f Mon Sep 17 00:00:00 2001 From: ragnep Date: Tue, 24 Mar 2026 12:24:59 +0200 Subject: [PATCH 3/5] wip Signed-off-by: ragnep --- components/waves/layout/WavesLayout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/waves/layout/WavesLayout.tsx b/components/waves/layout/WavesLayout.tsx index bcc5443356..82f6dc6cc9 100644 --- a/components/waves/layout/WavesLayout.tsx +++ b/components/waves/layout/WavesLayout.tsx @@ -67,7 +67,7 @@ function getNotAuthenticatedContent({ {activeWaveId === null ? ( } /> ) : ( From eb4e9b94bbdfb0c33d3bf1a55d5238d447a4c2d7 Mon Sep 17 00:00:00 2001 From: ragnep Date: Tue, 24 Mar 2026 12:25:59 +0200 Subject: [PATCH 4/5] wip Signed-off-by: ragnep --- components/waves/layout/WavesLayout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/waves/layout/WavesLayout.tsx b/components/waves/layout/WavesLayout.tsx index 82f6dc6cc9..b08f7efe75 100644 --- a/components/waves/layout/WavesLayout.tsx +++ b/components/waves/layout/WavesLayout.tsx @@ -67,7 +67,7 @@ function getNotAuthenticatedContent({ {activeWaveId === null ? ( } /> ) : ( From 8f7baaf67399d549e21e19e147a953339862f8c7 Mon Sep 17 00:00:00 2001 From: ragnep Date: Tue, 24 Mar 2026 12:28:18 +0200 Subject: [PATCH 5/5] wip Signed-off-by: ragnep --- components/waves/layout/WavesLayout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/waves/layout/WavesLayout.tsx b/components/waves/layout/WavesLayout.tsx index b08f7efe75..3e87aa1644 100644 --- a/components/waves/layout/WavesLayout.tsx +++ b/components/waves/layout/WavesLayout.tsx @@ -67,7 +67,7 @@ function getNotAuthenticatedContent({ {activeWaveId === null ? ( } /> ) : (