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
42 changes: 36 additions & 6 deletions src/common/fidgets/FidgetWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<FidgetSettings>;
} | undefined)?.lastFetchSettings;
const dataConfig = bundle.config?.data as
| { lastFetchSettings?: Partial<FidgetSettings>; 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<string, unknown>,
),
[bundle.properties.fields],
);

const derivedSettings = useMemo<FidgetSettings>(() => {
// 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;
}

Expand All @@ -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;
}

Expand All @@ -132,14 +155,21 @@ 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),
[derivedSettings, bundle.properties],
);

const shouldAttemptBackfill =
!disableBackfill &&
!!lastFetchSettings &&
!isEqual(derivedSettings, bundle.config.settings ?? {});

Expand Down
111 changes: 101 additions & 10 deletions src/fidgets/token/Portfolio.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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";
Expand Down Expand Up @@ -49,23 +58,18 @@ const portfolioProperties: FidgetProperties = {
{
fieldName: "farcasterUsername",
displayName: "Username",
default: "nounspacetom",
required: false,
disabledIf: (settings) => settings.trackType !== "farcaster",
inputSelector: (props) => (
<WithMargin>
<TextInput
{...props}
className="[&_label]:!normal-case"
/>
<PortfolioUsernameInput {...props} className="[&_label]:!normal-case" />
</WithMargin>
),
group: "settings",
},
{
fieldName: "walletAddresses",
displayName: "Address(es)",
default: "0x06AE622bF2029Db79Bdebd38F723f1f33f95F6C5",
required: false,
disabledIf: (settings) => settings.trackType !== "address",
inputSelector: (props) => (
Expand All @@ -90,6 +94,8 @@ const portfolioProperties: FidgetProperties = {

const Portfolio: React.FC<FidgetArgs<PortfolioFidgetSettings>> = ({
settings,
data,
saveData,
}) => {
const {
trackType,
Expand All @@ -100,12 +106,98 @@ const Portfolio: React.FC<FidgetArgs<PortfolioFidgetSettings>> = ({
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<string, unknown> } | 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<string, unknown> } | 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 (
Expand All @@ -128,4 +220,3 @@ export default {
fidget: Portfolio,
properties: portfolioProperties,
} as FidgetModule<FidgetArgs<PortfolioFidgetSettings>>;

92 changes: 92 additions & 0 deletions src/fidgets/token/PortfolioUsernameInput.tsx
Original file line number Diff line number Diff line change
@@ -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<string, unknown>) => 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<PortfolioInputProps> = ({
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<string>("");
const lastAutoUsernameRef = useRef<string>("");

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 (
<TextInput
{...rest}
value={value}
onChange={(next) => {
onChange?.(next);
}}
/>
);
};

export const getPortfolioPrimaryAddress = getPrimaryAddress;
export default PortfolioUsernameInput;