diff --git a/src/common/fidgets/FidgetWrapper.tsx b/src/common/fidgets/FidgetWrapper.tsx index e242de00e..237dd5711 100644 --- a/src/common/fidgets/FidgetWrapper.tsx +++ b/src/common/fidgets/FidgetWrapper.tsx @@ -90,14 +90,29 @@ export function FidgetWrapper({ // Generic settings backfill: any fidget can use lastFetchSettings in config.data // to automatically backfill empty settings. This is useful when fidgets are created // from external sources (e.g., URL parameters) and need to populate settings. - const lastFetchSettings = (bundle.config?.data as { - lastFetchSettings?: Partial; - } | undefined)?.lastFetchSettings; + const dataConfig = bundle.config?.data as + | { lastFetchSettings?: Partial; disableBackfill?: boolean } + | undefined; + const lastFetchSettings = dataConfig?.lastFetchSettings; + const disableBackfill = dataConfig?.disableBackfill === true; + + const defaultSettingsMap = useMemo( + () => + reduce( + bundle.properties.fields, + (acc, field) => { + acc[field.fieldName] = field.default; + return acc; + }, + {} as Record, + ), + [bundle.properties.fields], + ); const derivedSettings = useMemo(() => { // Use zustand settings directly (they're already the latest), fall back to props const baseSettings = (zustandSettings ?? bundle.config.settings ?? {}) as FidgetSettings; - if (!lastFetchSettings || typeof lastFetchSettings !== "object") { + if (disableBackfill || !lastFetchSettings || typeof lastFetchSettings !== "object") { return baseSettings; } @@ -107,8 +122,16 @@ export function FidgetWrapper({ // Helper to only fill empty settings const setValue = (key: string, value: unknown) => { const current = nextSettings[key]; + const defaultValue = defaultSettingsMap[key]; + const isDefaultValue = isEqual(current, defaultValue); + const isEmpty = + current === undefined || + current === null || + current === "" || + isDefaultValue; + // Don't overwrite existing non-empty values - if (current !== undefined && current !== null && current !== "") { + if (!isEmpty) { return; } @@ -132,7 +155,13 @@ export function FidgetWrapper({ }); return changed ? nextSettings : baseSettings; - }, [zustandSettings, bundle.config.settings, lastFetchSettings]); + }, [ + zustandSettings, + bundle.config.settings, + lastFetchSettings, + defaultSettingsMap, + disableBackfill, + ]); const settingsWithDefaults = useMemo( () => getSettingsWithDefaults(derivedSettings, bundle.properties), @@ -140,6 +169,7 @@ export function FidgetWrapper({ ); const shouldAttemptBackfill = + !disableBackfill && !!lastFetchSettings && !isEqual(derivedSettings, bundle.config.settings ?? {}); diff --git a/src/fidgets/token/Portfolio.tsx b/src/fidgets/token/Portfolio.tsx index 113c4185e..38c301807 100644 --- a/src/fidgets/token/Portfolio.tsx +++ b/src/fidgets/token/Portfolio.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useEffect, useMemo } from "react"; import TextInput from "@/common/components/molecules/TextInput"; import SettingsSelector from "@/common/components/molecules/SettingsSelector"; import { @@ -9,6 +9,15 @@ import { } from "@/common/fidgets"; import { defaultStyleFields, WithMargin } from "@/fidgets/helpers"; import { GiTwoCoins } from "react-icons/gi"; +import useCurrentFid from "@/common/lib/hooks/useCurrentFid"; +import { useLoadFarcasterUser } from "@/common/data/queries/farcaster"; +import { useNeynarUser } from "@/common/lib/hooks/useNeynarUser"; +import { useFarcasterSigner } from "@/fidgets/farcaster"; +import { useAppStore } from "@/common/data/stores/app"; +import PortfolioUsernameInput, { + getPortfolioPrimaryAddress, +} from "./PortfolioUsernameInput"; + export type PortfolioFidgetSettings = { trackType: "farcaster" | "address"; @@ -49,15 +58,11 @@ const portfolioProperties: FidgetProperties = { { fieldName: "farcasterUsername", displayName: "Username", - default: "nounspacetom", required: false, disabledIf: (settings) => settings.trackType !== "farcaster", inputSelector: (props) => ( - + ), group: "settings", @@ -65,7 +70,6 @@ const portfolioProperties: FidgetProperties = { { fieldName: "walletAddresses", displayName: "Address(es)", - default: "0x06AE622bF2029Db79Bdebd38F723f1f33f95F6C5", required: false, disabledIf: (settings) => settings.trackType !== "address", inputSelector: (props) => ( @@ -90,6 +94,8 @@ const portfolioProperties: FidgetProperties = { const Portfolio: React.FC> = ({ settings, + data, + saveData, }) => { const { trackType, @@ -100,12 +106,98 @@ const Portfolio: React.FC> = ({ fidgetShadow, } = settings; + const currentFid = useCurrentFid(); + const farcasterSigner = useFarcasterSigner("portfolio"); + const effectiveFid = (currentFid ?? farcasterSigner.fid) ?? -1; + const associatedFid = useAppStore( + (state) => state.account.getCurrentIdentity()?.associatedFids?.[0], + ); + const lookupFid = + effectiveFid > 0 ? effectiveFid : associatedFid ?? -1; + const { data: currentUserData } = useLoadFarcasterUser( + lookupFid, + lookupFid > 0 ? lookupFid : undefined, + ); + const loggedInUsername = useMemo(() => { + const username = currentUserData?.users?.[0]?.username; + return typeof username === "string" ? username.trim() : ""; + }, [currentUserData]); + + const normalizedUsername = useMemo( + () => (farcasterUsername || "").trim().replace(/^@/, ""), + [farcasterUsername], + ); + + const effectiveUsername = (normalizedUsername || loggedInUsername || "").toLowerCase(); + const { user: effectiveUser } = useNeynarUser( + effectiveUsername ? effectiveUsername : undefined, + ); + + const derivedAddresses = useMemo( + () => getPortfolioPrimaryAddress(effectiveUser), + [effectiveUser], + ); + + const resolvedFarcasterUsername = useMemo(() => { + const normalized = (farcasterUsername || "").trim().replace(/^@/, ""); + return normalized || loggedInUsername || ""; + }, [farcasterUsername, loggedInUsername]); + + const resolvedWalletAddresses = useMemo(() => { + const normalized = (walletAddresses || "").trim(); + return normalized; + }, [walletAddresses]); + + useEffect(() => { + if (normalizedUsername || !loggedInUsername) return; + + const previous = + (data as { lastFetchSettings?: { farcasterUsername?: string } } | undefined) + ?.lastFetchSettings?.farcasterUsername; + + if (previous === loggedInUsername) return; + + void saveData({ + ...(data || {}), + lastFetchSettings: { + ...(data as { lastFetchSettings?: Record } | undefined) + ?.lastFetchSettings, + farcasterUsername: loggedInUsername, + }, + }); + }, [normalizedUsername, loggedInUsername, data, saveData]); + + useEffect(() => { + if (!effectiveUsername || !derivedAddresses) return; + + const addressInput = (walletAddresses || "").trim(); + if (addressInput) return; + + const previous = + (data as { lastFetchSettings?: { walletAddresses?: string } } | undefined) + ?.lastFetchSettings?.walletAddresses; + if (previous === derivedAddresses) return; + + void saveData({ + ...(data || {}), + lastFetchSettings: { + ...(data as { lastFetchSettings?: Record } | undefined) + ?.lastFetchSettings, + walletAddresses: derivedAddresses, + }, + }); + }, [effectiveUsername, derivedAddresses, walletAddresses, data, saveData]); + const baseUrl = "https://balance-fidget.replit.app"; const url = trackType === "address" - ? `${baseUrl}/portfolio/${encodeURIComponent(walletAddresses)}` + ? resolvedWalletAddresses + ? `${baseUrl}/portfolio/${encodeURIComponent(resolvedWalletAddresses)}` + : baseUrl : trackType === "farcaster" - ? `${baseUrl}/fc/${encodeURIComponent(farcasterUsername)}` + ? resolvedFarcasterUsername + ? `${baseUrl}/fc/${encodeURIComponent(resolvedFarcasterUsername)}` + : baseUrl : baseUrl; return ( @@ -128,4 +220,3 @@ export default { fidget: Portfolio, properties: portfolioProperties, } as FidgetModule>; - diff --git a/src/fidgets/token/PortfolioUsernameInput.tsx b/src/fidgets/token/PortfolioUsernameInput.tsx new file mode 100644 index 000000000..ada6ca014 --- /dev/null +++ b/src/fidgets/token/PortfolioUsernameInput.tsx @@ -0,0 +1,92 @@ +import React, { useEffect, useMemo, useRef } from "react"; +import TextInput from "@/common/components/molecules/TextInput"; +import { useNeynarUser } from "@/common/lib/hooks/useNeynarUser"; +import useCurrentFid from "@/common/lib/hooks/useCurrentFid"; +import { useLoadFarcasterUser } from "@/common/data/queries/farcaster"; +import { useFarcasterSigner } from "@/fidgets/farcaster"; +import { useAppStore } from "@/common/data/stores/app"; + +type PortfolioInputProps = { + id?: string; + value: string; + onChange?: (value: string) => void; + updateSettings?: (partial: Record) => void; + className?: string; +}; + +const getPrimaryAddress = (user?: { + verifications?: string[]; + verified_addresses?: { eth_addresses?: string[] }; + custody_address?: string; +} | null) => { + const verifications = (user?.verifications || []) + .map((addr) => (typeof addr === "string" ? addr.trim() : "")) + .filter(Boolean); + if (verifications.length > 0) return verifications[0]!; + + const verified = (user?.verified_addresses?.eth_addresses || []) + .map((addr) => (typeof addr === "string" ? addr.trim() : "")) + .filter(Boolean); + if (verified.length > 0) return verified[0]!; + + const custody = user?.custody_address; + return typeof custody === "string" ? custody.trim() : ""; +}; + +const PortfolioUsernameInput: React.FC = ({ + value, + onChange, + updateSettings, + ...rest +}) => { + const currentFid = useCurrentFid(); + const farcasterSigner = useFarcasterSigner("portfolio-settings"); + const associatedFid = useAppStore( + (state) => state.account.getCurrentIdentity()?.associatedFids?.[0], + ); + const effectiveFid = (currentFid ?? farcasterSigner.fid) ?? -1; + const lookupFid = effectiveFid > 0 ? effectiveFid : associatedFid ?? -1; + const { data: currentUserData } = useLoadFarcasterUser( + lookupFid, + lookupFid > 0 ? lookupFid : undefined, + ); + const loggedInUsername = useMemo(() => { + const username = currentUserData?.users?.[0]?.username; + return typeof username === "string" ? username.trim() : ""; + }, [currentUserData]); + const normalized = (value || "").trim().replace(/^@/, ""); + const { user } = useNeynarUser(normalized ? normalized : undefined); + const primaryAddress = useMemo(() => getPrimaryAddress(user), [user]); + const lastAppliedRef = useRef(""); + const lastAutoUsernameRef = useRef(""); + + useEffect(() => { + if (normalized || !loggedInUsername || !updateSettings) return; + if (lastAutoUsernameRef.current === loggedInUsername) return; + lastAutoUsernameRef.current = loggedInUsername; + updateSettings({ farcasterUsername: loggedInUsername }); + }, [normalized, loggedInUsername, updateSettings]); + + useEffect(() => { + if (!normalized || !primaryAddress || !updateSettings) return; + if (lastAppliedRef.current === primaryAddress) return; + lastAppliedRef.current = primaryAddress; + updateSettings({ + farcasterUsername: normalized, + walletAddresses: primaryAddress, + }); + }, [normalized, primaryAddress, updateSettings]); + + return ( + { + onChange?.(next); + }} + /> + ); +}; + +export const getPortfolioPrimaryAddress = getPrimaryAddress; +export default PortfolioUsernameInput;