From b2e68715e35a5386fd2ebd494ac3a7acb46c258b Mon Sep 17 00:00:00 2001 From: Filipp Sher Date: Fri, 15 Nov 2024 18:12:16 +0200 Subject: [PATCH] Feature/discovery api (#39) * feat: implement mock hooks * chore: add interfaces * wip hooks implementation * fix after review * add auto fetching in hooks * upd hooks after review * fix: adjust effect and api calls for race conditions, image property now required * fix: handle unmount set state * chore: refactor abort to simple variable * chore: adjust not ok check in requests --------- Co-authored-by: Alexey Tsymbal --- packages/blinks-core/src/hooks/index.ts | 10 ++ .../blinks-core/src/hooks/useBlinkList.ts | 85 ++++++++++++++++ packages/blinks-core/src/hooks/useMetadata.ts | 96 +++++++++++++++++++ packages/blinks-core/src/utils/proxify.ts | 58 ++++++----- 4 files changed, 218 insertions(+), 31 deletions(-) create mode 100644 packages/blinks-core/src/hooks/useBlinkList.ts create mode 100644 packages/blinks-core/src/hooks/useMetadata.ts diff --git a/packages/blinks-core/src/hooks/index.ts b/packages/blinks-core/src/hooks/index.ts index 8685c4b0..3a3dff46 100644 --- a/packages/blinks-core/src/hooks/index.ts +++ b/packages/blinks-core/src/hooks/index.ts @@ -1,2 +1,12 @@ export { useAction } from './useAction'; export { useActionsRegistryInterval } from './useActionRegistryInterval'; +export { + useBlinkList, + type BlinkList, + type BlinkListEntry, +} from './useBlinkList.ts'; +export { + useMetadata, + type BlinkMetadata, + type MetadataRow, +} from './useMetadata'; diff --git a/packages/blinks-core/src/hooks/useBlinkList.ts b/packages/blinks-core/src/hooks/useBlinkList.ts new file mode 100644 index 00000000..b270861a --- /dev/null +++ b/packages/blinks-core/src/hooks/useBlinkList.ts @@ -0,0 +1,85 @@ +import { useCallback, useEffect, useState } from 'react'; +import { BLINK_CLIENT_KEY_HEADER, clientKey } from '../utils/client-key.ts'; + +export interface BlinkList { + entries: BlinkListEntry[]; +} + +export interface BlinkListEntry { + id: string; + title: string; + description: string; + blinkUrl: string; + metadataUrl?: string; + image: string; + icon?: string; +} + +export const useBlinkList = () => { + const [loading, setLoading] = useState(false); + const [data, setData] = useState(); + + const refetch = useCallback(() => { + let ignore = false; + + setLoading(true); + fetchBlinkList() + .then((data) => { + if (!ignore) { + setData(data); + } + }) + .finally(() => { + if (!ignore) { + setLoading(false); + } + }); + + return () => { + ignore = true; + }; + }, []); + + useEffect(() => { + const cancel = refetch(); + + return () => { + cancel(); + }; + }, [refetch]); + + return { + loading, + refetch, + data: data?.entries ?? [], + }; +}; + +async function fetchBlinkList(): Promise { + try { + const response = await fetch( + 'https://registry.dial.to/v1/private/blinks/list', + { + method: 'GET', + headers: { + Accept: 'application/json', + ...(clientKey && { [BLINK_CLIENT_KEY_HEADER]: clientKey }), + }, + }, + ); + if (!response.ok) { + console.error( + `[@dialectlabs/blinks] Failed to fetch blink list, response status: ${response.status}`, + ); + return { + entries: [], + }; + } + return (await response.json()) as BlinkList; + } catch (e) { + console.error(`[@dialectlabs/blinks] Failed to fetch blink list`, e); + return { + entries: [], + }; + } +} diff --git a/packages/blinks-core/src/hooks/useMetadata.ts b/packages/blinks-core/src/hooks/useMetadata.ts new file mode 100644 index 00000000..e19ddac9 --- /dev/null +++ b/packages/blinks-core/src/hooks/useMetadata.ts @@ -0,0 +1,96 @@ +import { useCallback, useEffect, useState } from 'react'; +import { proxifyMetadata } from '../utils/proxify.ts'; + +interface UseMetadataArgs { + wallet?: string; // user wallet address + url: string; // metadata url +} + +export interface BlinkMetadata { + rows: MetadataRow[]; + extendedDescription?: string; +} + +export interface MetadataRow { + key: string; + title: string; + value: string; + icon?: string; + url?: string; +} + +export const useMetadata = ({ url, wallet }: UseMetadataArgs) => { + const [loading, setLoading] = useState(false); + const [data, setData] = useState(); + + const refetch = useCallback(() => { + let ignore = false; + + setLoading(true); + fetchMetadata(url, wallet) + .then((data) => { + if (!ignore) { + setData(data); + } + }) + .finally(() => { + if (!ignore) { + setLoading(false); + } + }); + + return () => { + ignore = true; + }; + }, [url, wallet]); + + useEffect(() => { + const cancel = refetch(); + + return () => { + cancel(); + }; + }, [refetch]); + + return { + loading, + refetch, + data, + }; +}; + +async function fetchMetadata( + url: string, + wallet?: string, +): Promise { + try { + const urlObj = new URL(url); + if (wallet) { + urlObj.searchParams.append('account', wallet); + } + const { url: proxyUrl, headers: proxyHeaders } = proxifyMetadata( + urlObj.toString(), + ); + const response = await fetch(proxyUrl, { + method: 'GET', + headers: { + Accept: 'application/json', + ...proxyHeaders, + }, + }); + if (!response.ok) { + console.error( + `[@dialectlabs/blinks] Failed to fetch metadata, response status: ${response.status}`, + ); + return { + rows: [], + }; + } + return (await response.json()) as BlinkMetadata; + } catch (e) { + console.error(`[@dialectlabs/blinks] Failed to fetch metadata`, e); + return { + rows: [], + }; + } +} diff --git a/packages/blinks-core/src/utils/proxify.ts b/packages/blinks-core/src/utils/proxify.ts index 4ee6419c..84dd4ee1 100644 --- a/packages/blinks-core/src/utils/proxify.ts +++ b/packages/blinks-core/src/utils/proxify.ts @@ -2,6 +2,11 @@ import { BLINK_CLIENT_KEY_HEADER, clientKey } from './client-key.ts'; let proxyUrl: string | null = 'https://proxy.dial.to'; +export type ProxifiedResult = { + readonly url: URL; + readonly headers: Record; +}; + export function setProxyUrl(url: string): void { if (!url) { console.warn( @@ -21,38 +26,35 @@ export function setProxyUrl(url: string): void { proxyUrl = url; } -export function proxify(url: string): { - url: URL; - headers: Record; -} { - const baseUrl = new URL(url); - if (shouldIgnoreProxy(baseUrl)) { - return { - url: baseUrl, - headers: {}, - }; - } - const proxifiedUrl = new URL(proxyUrl!); - proxifiedUrl.searchParams.set('url', url); - return { - url: proxifiedUrl, - headers: getProxifiedHeaders(), - }; +export function proxify(url: string): ProxifiedResult { + return createProxifiedUrl(url); } -export function proxifyImage(url: string): { - url: URL; -} { - const baseUrl = new URL(url); - if (shouldIgnoreProxy(baseUrl)) { +export function proxifyImage(url: string): ProxifiedResult { + return createProxifiedUrl(url, 'image'); +} + +export function proxifyMetadata(url: string): ProxifiedResult { + return createProxifiedUrl(url, 'metadata'); +} + +function createProxifiedUrl(url: string, endpoint?: string): ProxifiedResult { + const incomingUrl = new URL(url); + if (!proxyUrl || shouldIgnoreProxy(incomingUrl)) { return { - url: baseUrl, + url: incomingUrl, + headers: {}, }; } - const proxifiedUrl = new URL(`${proxyUrl!}/image`); + + const proxifiedUrl = endpoint + ? new URL(endpoint, proxyUrl) + : new URL(proxyUrl); proxifiedUrl.searchParams.set('url', url); + return { url: proxifiedUrl, + headers: getProxifiedHeaders(), }; } @@ -63,11 +65,5 @@ function getProxifiedHeaders(): Record { } function shouldIgnoreProxy(url: URL): boolean { - if (url.hostname === 'localhost' || url.hostname === '127.0.0.1') { - return true; - } - if (!proxyUrl) { - return true; - } - return false; + return url.hostname === 'localhost' || url.hostname === '127.0.0.1'; }