Skip to content

Commit

Permalink
feat: mini app support
Browse files Browse the repository at this point in the history
  • Loading branch information
michalkvasnicak committed Nov 4, 2024
1 parent d7fcc19 commit 4890662
Show file tree
Hide file tree
Showing 11 changed files with 1,017 additions and 54 deletions.
13 changes: 12 additions & 1 deletion packages/render/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,16 @@
"default": "./dist/use-frame.cjs"
}
},
"./use-composer-action": {
"import": {
"types": "./dist/use-composer-action.d.ts",
"default": "./dist/use-composer-action.js"
},
"require": {
"types": "./dist/use-composer-action.d.cts",
"default": "./dist/use-composer-action.cjs"
}
},
"./unstable-use-debugger-frame-state": {
"import": {
"types": "./dist/unstable-use-debugger-frame-state.d.ts",
Expand Down Expand Up @@ -285,6 +295,7 @@
"dependencies": {
"@farcaster/core": "^0.14.7",
"@noble/ed25519": "^2.0.0",
"frames.js": "^0.19.5"
"frames.js": "^0.19.5",
"zod": "^3.23.8"
}
}
6 changes: 6 additions & 0 deletions packages/render/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,9 @@ export class ComposerActionUnexpectedResponseError extends Error {
super("Unexpected composer action response from the server");
}
}

export class ComposerActionUserRejectedRequestError extends Error {
constructor() {
super("User rejected the request");
}
}
75 changes: 74 additions & 1 deletion packages/render/src/farcaster/frames.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,53 @@ import {
getFarcasterTime,
makeFrameAction,
} from "@farcaster/core";
import { hexToBytes } from "viem";
import { bytesToHex, hexToBytes } from "viem";
import type {
FrameActionBodyPayload,
FrameContext,
SignedFrameAction,
SignerStateActionContext,
SignFrameActionFunc,
} from "../types";
import type {
SignComposerActionFunc,
SignerStateComposerActionContext,
} from "../unstable-types";
import { tryCallAsync } from "../helpers";
import type { FarcasterSigner } from "./signers";
import type { FarcasterFrameContext } from "./types";

/**
* Creates a singer request payload to fetch composer action url.
*/
export const signComposerAction: SignComposerActionFunc =
async function signComposerAction(signerPrivateKey, actionContext) {
const messageOrError = await tryCallAsync(() =>
createComposerActionMessageWithSignerKey(signerPrivateKey, actionContext)
);

if (messageOrError instanceof Error) {
throw messageOrError;
}

const { message, trustedBytes } = messageOrError;

return {
untrustedData: {
buttonIndex: 1,
fid: actionContext.fid,
messageHash: bytesToHex(message.hash),
network: 1,
state: Buffer.from(message.data.frameActionBody.state).toString(),
timestamp: new Date().getTime(),
url: actionContext.url,
},
trustedData: {
messageBytes: trustedBytes,
},
};
};

/** Creates a frame action for use with `useFrame` and a proxy */
export const signFrameAction: SignFrameActionFunc<
FarcasterSigner,
Expand Down Expand Up @@ -104,6 +140,43 @@ export const signFrameAction: SignFrameActionFunc<
};
};

export async function createComposerActionMessageWithSignerKey(
signerKey: string,
{ fid, state, url }: SignerStateComposerActionContext
): Promise<{
message: FrameActionMessage;
trustedBytes: string;
}> {
const signer = new NobleEd25519Signer(Buffer.from(signerKey.slice(2), "hex"));

const messageDataOptions = {
fid,
network: FarcasterNetwork.MAINNET,
};

const message = await makeFrameAction(
FrameActionBody.create({
url: Buffer.from(url),
buttonIndex: 1,
state: Buffer.from(encodeURIComponent(JSON.stringify({ cast: state }))),
}),
messageDataOptions,
signer
);

if (message.isErr()) {
throw message.error;
}

const messageData = message.value;

const trustedBytes = Buffer.from(
Message.encode(message._unsafeUnwrap()).finish()
).toString("hex");

return { message: messageData, trustedBytes };
}

export async function createFrameActionMessageWithSignerKey(
signerKey: string,
{
Expand Down
5 changes: 4 additions & 1 deletion packages/render/src/farcaster/signers.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import type { FrameActionBodyPayload, SignerStateInstance } from "../types";
import type { SignComposerActionFunc } from "../unstable-types";
import type { FarcasterFrameContext } from "./types";

export type FarcasterSignerState<TSignerType = FarcasterSigner | null> =
SignerStateInstance<
TSignerType,
FrameActionBodyPayload,
FarcasterFrameContext
>;
> & {
signComposerAction: SignComposerActionFunc;
};

export type FarcasterSignerPendingApproval = {
status: "pending_approval";
Expand Down
35 changes: 35 additions & 0 deletions packages/render/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type {
ParseFramesWithReportsResult,
ParseResult,
} from "frames.js/frame-parsers";
import type { ComposerActionFormResponse } from "frames.js/types";
import type { PartialFrame } from "./ui/types";

export async function tryCallAsync<TResult>(
Expand Down Expand Up @@ -71,3 +72,37 @@ export function isPartialFrame(
value.frame.buttons.length > 0
);
}

export function isComposerFormActionResponse(
response: unknown
): response is ComposerActionFormResponse {
return (
typeof response === "object" &&
response !== null &&
"type" in response &&
response.type === "form"
);
}

/**
* Merges all search params in order from left to right into the URL.
*
* @param url - The URL to merge the search params into. Either fully qualified or path only.
*/
export function mergeSearchParamsToUrl(
url: string,
...searchParams: URLSearchParams[]
): string {
const temporaryDomain = "temporary-for-parsing-purposes.tld";
const parsedProxyUrl = new URL(url, `http://${temporaryDomain}`);

searchParams.forEach((params) => {
params.forEach((value, key) => {
parsedProxyUrl.searchParams.set(key, value);
});
});

return parsedProxyUrl.hostname === temporaryDomain
? `${parsedProxyUrl.pathname}${parsedProxyUrl.search}`
: parsedProxyUrl.toString();
}
31 changes: 15 additions & 16 deletions packages/render/src/identity/farcaster/use-farcaster-identity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ import {
} from "react";
import { convertKeypairToHex, createKeypairEDDSA } from "../crypto";
import type { FarcasterSignerState } from "../../farcaster";
import { signFrameAction } from "../../farcaster";
import { signComposerAction, signFrameAction } from "../../farcaster";
import type { Storage } from "../types";
import { useVisibilityDetection } from "../../hooks/use-visibility-detection";
import { WebStorage } from "../storage";
import { useStorage } from "../../hooks/use-storage";
import { useFreshRef } from "../../hooks/use-fresh-ref";
import { IdentityPoller } from "./identity-poller";
import type {
FarcasterCreateSignerResult,
Expand Down Expand Up @@ -187,16 +188,12 @@ export function useFarcasterIdentity({
return value;
},
});
const onImpersonateRef = useRef(onImpersonate);
onImpersonateRef.current = onImpersonate;
const onLogInRef = useRef(onLogIn);
onLogInRef.current = onLogIn;
const onLogInStartRef = useRef(onLogInStart);
onLogInStartRef.current = onLogInStart;
const onLogOutRef = useRef(onLogOut);
onLogOutRef.current = onLogOut;
const generateUserIdRef = useRef(generateUserId);
generateUserIdRef.current = generateUserId;
const onImpersonateRef = useFreshRef(onImpersonate);
const onLogInRef = useFreshRef(onLogIn);
const onLogInStartRef = useFreshRef(onLogInStart);
const onLogOutRef = useFreshRef(onLogOut);
const generateUserIdRef = useFreshRef(generateUserId);
const onMissingIdentityRef = useFreshRef(onMissingIdentity);

const createFarcasterSigner =
useCallback(async (): Promise<FarcasterCreateSignerResult> => {
Expand Down Expand Up @@ -300,7 +297,7 @@ export function useFarcasterIdentity({
console.error("@frames.js/render: API Call failed", error);
throw error;
}
}, [setState, signerUrl]);
}, [generateUserIdRef, onLogInStartRef, setState, signerUrl]);

const impersonateUser = useCallback(
async (fid: number) => {
Expand Down Expand Up @@ -329,14 +326,14 @@ export function useFarcasterIdentity({
setIsLoading(false);
}
},
[setState]
[generateUserIdRef, onImpersonateRef, setState]
);

const onSignerlessFramePress = useCallback((): Promise<void> => {
onMissingIdentity();
onMissingIdentityRef.current();

return Promise.resolve();
}, [onMissingIdentity]);
}, [onMissingIdentityRef]);

const createSigner = useCallback(async () => {
setIsLoading(true);
Expand All @@ -354,7 +351,7 @@ export function useFarcasterIdentity({

return identityReducer(currentState, { type: "LOGOUT" });
});
}, [setState]);
}, [onLogOutRef, setState]);

const farcasterUser = state.status === "init" ? null : state;

Expand Down Expand Up @@ -418,6 +415,7 @@ export function useFarcasterIdentity({
visibilityDetector,
setState,
enableIdentityPolling,
onLogInRef,
]);

return useMemo(
Expand All @@ -428,6 +426,7 @@ export function useFarcasterIdentity({
farcasterUser?.status === "approved" ||
farcasterUser?.status === "impersonating",
signFrameAction,
signComposerAction,
isLoadingSigner: isLoading,
impersonateUser,
onSignerlessFramePress,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ import {
} from "react";
import { convertKeypairToHex, createKeypairEDDSA } from "../crypto";
import type { FarcasterSignerState } from "../../farcaster";
import { signFrameAction } from "../../farcaster";
import { signComposerAction, signFrameAction } from "../../farcaster";
import type { Storage } from "../types";
import { useVisibilityDetection } from "../../hooks/use-visibility-detection";
import { WebStorage } from "../storage";
import { useStorage } from "../../hooks/use-storage";
import { useFreshRef } from "../../hooks/use-fresh-ref";
import { IdentityPoller } from "./identity-poller";
import type {
FarcasterCreateSignerResult,
Expand Down Expand Up @@ -259,20 +260,14 @@ export function useFarcasterMultiIdentity({
identities: [],
},
});
const onImpersonateRef = useRef(onImpersonate);
onImpersonateRef.current = onImpersonate;
const onLogInRef = useRef(onLogIn);
onLogInRef.current = onLogIn;
const onLogInStartRef = useRef(onLogInStart);
onLogInStartRef.current = onLogInStart;
const onLogOutRef = useRef(onLogOut);
onLogOutRef.current = onLogOut;
const onIdentityRemoveRef = useRef(onIdentityRemove);
onIdentityRemoveRef.current = onIdentityRemove;
const onIdentitySelectRef = useRef(onIdentitySelect);
onIdentitySelectRef.current = onIdentitySelect;
const generateUserIdRef = useRef(generateUserId);
generateUserIdRef.current = generateUserId;
const onImpersonateRef = useFreshRef(onImpersonate);
const onLogInRef = useFreshRef(onLogIn);
const onLogInStartRef = useFreshRef(onLogInStart);
const onLogOutRef = useFreshRef(onLogOut);
const onIdentityRemoveRef = useFreshRef(onIdentityRemove);
const onIdentitySelectRef = useFreshRef(onIdentitySelect);
const generateUserIdRef = useFreshRef(generateUserId);
const onMissingIdentityRef = useFreshRef(onMissingIdentity);

const createFarcasterSigner =
useCallback(async (): Promise<FarcasterCreateSignerResult> => {
Expand Down Expand Up @@ -388,7 +383,7 @@ export function useFarcasterMultiIdentity({
console.error("@frames.js/render: API Call failed", error);
throw error;
}
}, [setState, signerUrl]);
}, [generateUserIdRef, onLogInStartRef, setState, signerUrl]);

const impersonateUser = useCallback(
async (fid: number) => {
Expand Down Expand Up @@ -417,14 +412,14 @@ export function useFarcasterMultiIdentity({
setIsLoading(false);
}
},
[setState]
[generateUserIdRef, onImpersonateRef, setState]
);

const onSignerlessFramePress = useCallback((): Promise<void> => {
onMissingIdentity();
onMissingIdentityRef.current();

return Promise.resolve();
}, [onMissingIdentity]);
}, [onMissingIdentityRef]);

const createSigner = useCallback(async () => {
setIsLoading(true);
Expand All @@ -442,7 +437,7 @@ export function useFarcasterMultiIdentity({

return identityReducer(currentState, { type: "LOGOUT" });
});
}, [setState]);
}, [onLogOutRef, setState]);

const removeIdentity = useCallback(async () => {
await setState((currentState) => {
Expand All @@ -452,7 +447,7 @@ export function useFarcasterMultiIdentity({

return identityReducer(currentState, { type: "REMOVE" });
});
}, [setState]);
}, [onIdentityRemoveRef, setState]);

const farcasterUser = state.activeIdentity;

Expand Down Expand Up @@ -534,6 +529,7 @@ export function useFarcasterMultiIdentity({
farcasterUser?.status === "approved" ||
farcasterUser?.status === "impersonating",
signFrameAction,
signComposerAction,
isLoadingSigner: isLoading,
impersonateUser,
onSignerlessFramePress,
Expand Down
Loading

0 comments on commit 4890662

Please sign in to comment.