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
45 changes: 7 additions & 38 deletions src/app/(spaces)/PublicSpace.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
"use client";

import React from "react";
import { useAuthenticatorManager } from "@/authenticators/AuthenticatorManager";
import { useSidebarContext } from "@/common/components/organisms/Sidebar";
import TabBar from "@/common/components/organisms/TabBar";
import { useAppStore } from "@/common/data/stores/app";
import { useCurrentFid } from "@/common/lib/hooks/useCurrentFid";
import { EtherScanChainName } from "@/constants/etherscanChainIds";
import { INITIAL_SPACE_CONFIG_EMPTY } from "@/config";
import Profile from "@/fidgets/ui/profile";
import Channel from "@/fidgets/ui/channel";
import { useWallets } from "@privy-io/react-auth";
import { indexOf, isNil, mapValues, noop} from "lodash";
import { isNil, mapValues, noop} from "lodash";
import { useRouter } from "next/navigation";
import { useCallback, useEffect, useMemo, useState } from "react";
import { Address } from "viem";
import { SpaceConfigSaveDetails } from "./Space";
import { toast } from "sonner";
import SpacePage from "./SpacePage";
import {
SpacePageData,
Expand All @@ -23,8 +24,6 @@ import {
isProposalSpace,
isChannelSpace,
} from "@/common/types/spaceData";
const FARCASTER_NOUNSPACE_AUTHENTICATOR_NAME = "farcaster:nounspace";

interface PublicSpaceProps {
spacePageData: SpacePageData;
tabName: string;
Expand Down Expand Up @@ -107,40 +106,9 @@ export default function PublicSpace({
const currentConfig = getConfig();

// Identity states
const [currentUserFid, setCurrentUserFid] = useState<number | null>(null);
const [isSignedIntoFarcaster, setIsSignedIntoFarcaster] = useState(false);
const currentUserFid = useCurrentFid();
const { wallets } = useWallets();

const {
lastUpdatedAt: authManagerLastUpdatedAt,
getInitializedAuthenticators: authManagerGetInitializedAuthenticators,
callMethod: authManagerCallMethod,
} = useAuthenticatorManager();

// Checks if the user is signed into Farcaster
useEffect(() => {
authManagerGetInitializedAuthenticators().then((authNames) => {
setIsSignedIntoFarcaster(
indexOf(authNames, FARCASTER_NOUNSPACE_AUTHENTICATOR_NAME) > -1,
);
});
}, [authManagerLastUpdatedAt]);

// Loads the current user's FID if they're signed into Farcaster
useEffect(() => {
if (!isSignedIntoFarcaster) return;
authManagerCallMethod({
requestingFidgetId: "root",
authenticatorId: FARCASTER_NOUNSPACE_AUTHENTICATOR_NAME,
methodName: "getAccountFid",
isLookup: true,
}).then((authManagerResp) => {
if (authManagerResp.result === "success") {
setCurrentUserFid(authManagerResp.value as number);
}
});
}, [isSignedIntoFarcaster, authManagerLastUpdatedAt]);

// Load editable spaces when user signs in
useEffect(() => {
if (!currentUserFid) return;
Expand Down Expand Up @@ -174,7 +142,7 @@ export default function PublicSpace({
);

return result;
}, [spacePageData, currentUserFid, wallets, isSignedIntoFarcaster]);
}, [spacePageData, currentUserFid, wallets]);

// Config logic:
// - If we have currentTabName and the tab is loaded in store, use it
Expand Down Expand Up @@ -347,7 +315,8 @@ export default function PublicSpace({
const saveConfig = useCallback(
async (spaceConfig: SpaceConfigSaveDetails) => {
if (isNil(currentSpaceId) || isNil(currentTabName)) {
throw new Error("Cannot save config until space and tab are initialized");
toast.error("Space is still initializing. Try again in a moment.");
return;
}
const saveableConfig = {
...spaceConfig,
Expand Down
24 changes: 12 additions & 12 deletions src/app/(spaces)/s/[handle]/ProfileSpace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@
* Integrates with: PublicSpace
*/

import React, { useMemo } from "react";
import PublicSpace from "@/app/(spaces)/PublicSpace";
import { ProfileSpacePageData } from "@/common/types/spaceData";
import { useCurrentSpaceIdentityPublicKey } from "@/common/lib/hooks/useCurrentSpaceIdentityPublicKey";
import { ProfileSpacePageData } from "@/common/types/spaceData";
import React ,{ useMemo } from "react";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix formatting in import statement.

There's an extra space before the comma in the React import.

🔧 Proposed fix
-import React ,{ useMemo } from "react";
+import React, { useMemo } from "react";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import React ,{ useMemo } from "react";
import React, { useMemo } from "react";
🤖 Prompt for AI Agents
In `@src/app/`(spaces)/s/[handle]/ProfileSpace.tsx at line 30, The import line in
ProfileSpace.tsx has a stray space before the comma in "import React ,{ useMemo
} from \"react\""; update the import to the correct formatting by removing the
extra space so it reads "React, { useMemo }" (locate the import statement at the
top of ProfileSpace.tsx). Ensure spacing around the comma matches other imports
in the repo.


export interface ProfileSpaceProps {
spacePageData: Omit<ProfileSpacePageData, 'isEditable' | 'spacePageUrl'>;
Expand All @@ -43,19 +43,19 @@ const isProfileSpaceEditable = (
currentUserIdentityPublicKey?: string
): boolean => {
// Require user to be logged in (have an identity key)
if (!currentUserIdentityPublicKey) {
console.log('[ProfileSpace] User not logged in - not editable');
return false;
}
// if (!currentUserIdentityPublicKey) {
// console.log('[ProfileSpace] User not logged in - not editable');
// return false;
// }

// Check FID ownership (original logic)
const hasFidOwnership =
currentUserFid !== undefined &&
spaceOwnerFid !== undefined &&
const hasFidOwnership =
currentUserFid !== undefined &&
spaceOwnerFid !== undefined &&
currentUserFid === spaceOwnerFid;

// Check identity key ownership (only if space is registered)
const hasIdentityOwnership = !!(spaceId && spaceIdentityPublicKey &&
const hasIdentityOwnership = !!(spaceId && spaceIdentityPublicKey &&
spaceIdentityPublicKey === currentUserIdentityPublicKey);

console.log('[ProfileSpace] Editability check details:', {
Expand All @@ -82,9 +82,9 @@ export default function ProfileSpace({
const spaceDataWithClientSideLogic = useMemo(() => ({
...spaceData,
spacePageUrl: (tabName: string) => `/s/${spaceData.spaceName}/${encodeURIComponent(tabName)}`,
isEditable: (currentUserFid: number | undefined) =>
isEditable: (currentUserFid: number | undefined) =>
isProfileSpaceEditable(
spaceData.spaceOwnerFid,
spaceData.spaceOwnerFid,
currentUserFid,
spaceData.spaceId,
spaceData.identityPublicKey,
Expand Down
39 changes: 22 additions & 17 deletions src/authenticators/AuthenticatorManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
AuthenticatorMethods,
} from ".";
import authenticators from "./authenticators";
import { SetupStep } from "@/common/data/stores/app/setup";

type AuthenticatorPermissions = {
[fidgetId: string]: string[];
Expand Down Expand Up @@ -155,9 +156,10 @@ export const AuthenticatorManagerProvider: React.FC<
}>();
const [initializationQueue, setInitializationQueue] = useState<string[]>([]);

const { modalOpen, setModalOpen } = useAppStore((state) => ({
const { modalOpen, setModalOpen, currentStep } = useAppStore((state) => ({
modalOpen: state.setup.modalOpen,
setModalOpen: state.setup.setModalOpen
setModalOpen: state.setup.setModalOpen,
currentStep: state.setup.currentStep,
}));

const authenticatorManager = useMemo<AuthenticatorManager>(
Expand All @@ -174,7 +176,9 @@ export const AuthenticatorManagerProvider: React.FC<
// TO DO: When adding permissioning
// Allow client requests to not open modal
// While Fidget requests will
if (!modalOpen && !isLookup) {
// Only auto-open once the user is fully in-app; during login/setup this causes
// a confusing "blank" modal that closes later.
if (!modalOpen && !isLookup && currentStep === SetupStep.DONE) {
setModalOpen(true);
}
return {
Expand Down Expand Up @@ -242,20 +246,21 @@ export const AuthenticatorManagerProvider: React.FC<
});
},
initializeAuthenticators: (authenticatorIds) => {
setInitializationQueue(concat(initializationQueue, authenticatorIds));
setInitializationQueue((queue) => concat(queue, authenticatorIds));
},
CurrentInitializerComponent: () =>
currentInitializer && (
<currentInitializer.initializer
data={authenticatorConfig[currentInitializer.id].data}
saveData={saveSingleAuthenticatorData(
authenticatorConfig,
currentInitializer.id,
authenticatorConfig[currentInitializer.id],
)}
done={completeInstallingCurrentInitializer}
/>
),
CurrentInitializerComponent: currentInitializer
? () => (
<currentInitializer.initializer
data={authenticatorConfig[currentInitializer.id].data}
saveData={saveSingleAuthenticatorData(
authenticatorConfig,
currentInitializer.id,
authenticatorConfig[currentInitializer.id],
)}
done={completeInstallingCurrentInitializer}
/>
)
: undefined,
lastUpdatedAt: moment().toISOString(),
}),
[
Expand All @@ -274,7 +279,7 @@ export const AuthenticatorManagerProvider: React.FC<
isUndefined(authenticator) ||
(await authenticator.methods.isReady())
) {
setInitializationQueue(tail(initializationQueue));
setInitializationQueue((queue) => tail(queue));
return;
} else {
setCurrentInitializer({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,20 +226,17 @@ const initializer: AuthenticatorInitializer<
});

function devSignin() {
// In development, generate test signing keys so Quick Auth can work
// These are random keys - not linked to a real Farcaster account
const newPrivKey = ed25519.utils.randomPrivateKey();
const publicKeyHex = `0x${bytesToHex(ed25519.getPublicKey(newPrivKey))}`;
const privateKeyHex = `0x${bytesToHex(newPrivKey)}`;

saveData({
...data,
status: "completed",
accountFid: Number(devFid),
accountType: "signer", // Use signer type so keys are available
publicKeyHex: publicKeyHex,
privateKeyHex: privateKeyHex,
accountType: "account",
publicKeyHex,
privateKeyHex: `0x${bytesToHex(newPrivKey)}`,
});
done();
}

const warpcastSignerUrl = data.signerUrl
Expand Down
13 changes: 6 additions & 7 deletions src/common/components/templates/LoginModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,20 +76,19 @@ const LoginModal = ({
) : null;
}

if (currentStep === SetupStep.REQUIRED_AUTHENTICATORS_INSTALLED)
return CurrentInitializerComponent ? (
<CurrentInitializerComponent />
) : (
"One second..."
);
// If an authenticator is requesting initialization (e.g. Farcaster signer),
// show its initializer regardless of the current setup step.
if (CurrentInitializerComponent) {
return <CurrentInitializerComponent />;
}

return <LoadingScreen text={currentStep} />;
}

return (
<Modal
setOpen={setOpen}
open={open && authenticated && currentStep !== SetupStep.DONE}
open={open && authenticated && (currentStep !== SetupStep.DONE || !!CurrentInitializerComponent)}
showClose={showClose}
>
{getModalContent()}
Expand Down
65 changes: 43 additions & 22 deletions src/common/data/stores/app/accounts/farcasterStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,57 +7,81 @@ import { AppStore } from "..";
import { StoreGet, StoreSet } from "../../createStore";
import axiosBackend from "../../../api/backend";
import { concat, isUndefined } from "lodash";
import { hashObject } from "@/common/lib/signedFiles";
import { signSignable , hashObject} from "@/common/lib/signedFiles";
import moment from "moment";
import { bytesToHex } from "@noble/ciphers/utils";

import { AnalyticsEvent } from "@/common/constants/analyticsEvents";
import { analytics } from "@/common/providers/AnalyticsProvider";

import { InferFidLinkRequest, InferFidLinkResponse } from "@/pages/api/fid-link/infer";

type FarcasterActions = {
getFidsForCurrentIdentity: () => Promise<void>;
inferFidForCurrentIdentity: (walletAddress: string) => Promise<number | null>;
registerFidForCurrentIdentity: (
fid: number,
signingKey: string,
// Takes in signMessage as it is a method
// of the Authenticator and client doesn't
// have direct access to the keys
signMessage: (messageHash: Uint8Array) => Promise<Uint8Array>,
signMessage: (messageHash: Uint8Array) => Promise<Uint8Array>
) => Promise<void>;
setFidsForCurrentIdentity: (fids: number[]) => void;
addFidToCurrentIdentity: (fid: number) => void;
};

export type FarcasterStore = FarcasterActions;

export const farcasterStore = (
set: StoreSet<AppStore>,
get: StoreGet<AppStore>,
): FarcasterStore => ({
export const farcasterStore = (set: StoreSet<AppStore>, get: StoreGet<AppStore>): FarcasterStore => ({
addFidToCurrentIdentity: (fid) => {
const currentFids =
get().account.getCurrentIdentity()?.associatedFids || [];
const currentFids = get().account.getCurrentIdentity()?.associatedFids || [];
get().account.setFidsForCurrentIdentity(concat(currentFids, [fid]));
},
setFidsForCurrentIdentity: (fids) => {
set((draft) => {
draft.account.spaceIdentities[
draft.account.getCurrentIdentityIndex()
].associatedFids = fids;
draft.account.spaceIdentities[draft.account.getCurrentIdentityIndex()].associatedFids = fids;
}, "setFidsForCurrentIdentity");
},
getFidsForCurrentIdentity: async () => {
const { data } = await axiosBackend.get<FidsLinkedToIdentityResponse>(
"/api/fid-link",
{
params: {
identityPublicKey: get().account.currentSpaceIdentityPublicKey,
},
const { data } = await axiosBackend.get<FidsLinkedToIdentityResponse>("/api/fid-link", {
params: {
identityPublicKey: get().account.currentSpaceIdentityPublicKey,
},
);
});
if (!isUndefined(data.value)) {
get().account.setFidsForCurrentIdentity(data.value!.fids);
}
},
inferFidForCurrentIdentity: async (walletAddress) => {
try {
const identity = get().account.getCurrentIdentity();
const identityPublicKey = identity?.rootKeys?.publicKey;
const identityPrivateKey = identity?.rootKeys?.privateKey;
if (!identityPublicKey || !identityPrivateKey) return null;

const unsigned: Omit<InferFidLinkRequest, "signature"> = {
identityPublicKey,
walletAddress: walletAddress.toLowerCase(),
timestamp: moment().toISOString(),
};

// Use signSignable helper for consistency with the rest of the codebase
const signed = signSignable(unsigned, identityPrivateKey) as InferFidLinkRequest;

const { data } = await axiosBackend.post<InferFidLinkResponse>("/api/fid-link/infer", signed);
if (!data.value) return null;
get().account.addFidToCurrentIdentity(data.value.fid);
analytics.track(AnalyticsEvent.LINK_FID, {
fid: data.value.fid,
inferred: true,
});
return data.value.fid;
} catch (e) {
console.error("[inferFidForCurrentIdentity] failed", e);
return null;
}
},
registerFidForCurrentIdentity: async (fid, signingKey, signMessage) => {
const request: Omit<FidLinkToIdentityRequest, "signature"> = {
fid,
Expand All @@ -69,10 +93,7 @@ export const farcasterStore = (
...request,
signature: bytesToHex(await signMessage(hashObject(request))),
};
const { data } = await axiosBackend.post<FidLinkToIdentityResponse>(
"/api/fid-link",
signedRequest,
);
const { data } = await axiosBackend.post<FidLinkToIdentityResponse>("/api/fid-link", signedRequest);
if (!isUndefined(data.value)) {
get().account.addFidToCurrentIdentity(data.value!.fid);
analytics.track(AnalyticsEvent.LINK_FID, { fid });
Expand Down
Loading