diff --git a/apps/website/.env b/apps/website/.env index 8a7fcfdd..c5aff7bd 100644 --- a/apps/website/.env +++ b/apps/website/.env @@ -1,3 +1,4 @@ IGNORE_TS_CONFIG_PATHS=true TAMAGUI_TARGET=web TAMAGUI_DISABLE_WARN_DYNAMIC_LOAD=1 + diff --git a/apps/website/env.d.ts b/apps/website/env.d.ts new file mode 100644 index 00000000..3870ac51 --- /dev/null +++ b/apps/website/env.d.ts @@ -0,0 +1,11 @@ +import type { env } from './src/config/env.mjs' + +type Env = typeof env + +declare global { + namespace NodeJS { + /* eslint-disable @typescript-eslint/no-empty-interface */ + interface ProcessEnv extends Env {} + /* eslint-enable @typescript-eslint/no-empty-interface */ + } +} diff --git a/apps/website/next.config.js b/apps/website/next.config.mjs similarity index 86% rename from apps/website/next.config.js rename to apps/website/next.config.mjs index aa85ba48..0bed29ea 100644 --- a/apps/website/next.config.js +++ b/apps/website/next.config.mjs @@ -1,9 +1,12 @@ /* eslint-disable eslint-comments/disable-enable-pair */ -/* eslint-disable @typescript-eslint/no-var-requires */ +/* eslint-disable import/default */ -/** @type {import('next').NextConfig} */ -const { withTamagui } = require('@tamagui/next-plugin') -const { join } = require('path') +import './src/config/env.mjs' + +import tamagui_next_plugin from '@tamagui/next-plugin' +import { join } from 'node:path' + +const { withTamagui } = tamagui_next_plugin /** @type {import('next').NextConfig} */ let config = { @@ -59,7 +62,7 @@ const plugins = [ }), ] -module.exports = () => { +export default () => { for (const plugin of plugins) { config = { ...config, diff --git a/apps/website/package.json b/apps/website/package.json index 6c288006..a8b5f4b2 100644 --- a/apps/website/package.json +++ b/apps/website/package.json @@ -21,9 +21,10 @@ "@status-im/icons": "*", "@status-im/js": "*", "@tamagui/next-theme": "1.11.1", + "@tanstack/react-query": "^4.29.7", "@vercel/og": "^0.5.4", - "class-variance-authority": "^0.6.0", "@visx/visx": "^2.18.0", + "class-variance-authority": "^0.6.0", "d3-array": "^3.2.3", "d3-time-format": "^4.1.0", "next": "13.2.4", @@ -31,8 +32,8 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-native-web": "^0.18.12", - "@tanstack/react-query": "^4.29.7", - "ts-pattern": "^4.3.0" + "ts-pattern": "^4.3.0", + "zod": "^3.21.4" }, "devDependencies": { "@achingbrain/ssdp": "^4.0.1", diff --git a/apps/website/src/components/preview-page.tsx b/apps/website/src/components/preview-page.tsx index 2ca361d1..61aac8c3 100644 --- a/apps/website/src/components/preview-page.tsx +++ b/apps/website/src/components/preview-page.tsx @@ -109,8 +109,10 @@ export function PreviewPage(props: PreviewPageProps) { channelUuid: urlChannelUuid, data: urlData, errorCode: urlErrorCode, + isLoading: urlIsLoading, } = useURLData(type, decodedData, encodedData) + const wakuQueryIsEnabled = Boolean(publicKey) const { data: wakuData, isLoading, @@ -119,7 +121,7 @@ export function PreviewPage(props: PreviewPageProps) { } = useQuery({ refetchOnWindowFocus: false, queryKey: [type], - enabled: !!publicKey, + enabled: wakuQueryIsEnabled, queryFn: async function ({ queryKey }): Promise { const client = await getRequestClient() @@ -179,7 +181,20 @@ export function PreviewPage(props: PreviewPageProps) { }, }) - const loading = status === 'loading' || isLoading + const loading = getLoading() + + function getLoading(): boolean { + if (urlIsLoading) { + return true + } + + if (wakuQueryIsEnabled) { + return status === 'loading' || isLoading + } + + return false + } + const data: Data | undefined = wakuData ?? urlData const { avatarURL, bannerURL } = useMemo(() => { diff --git a/apps/website/src/config/env.mjs b/apps/website/src/config/env.mjs new file mode 100644 index 00000000..93e0ee11 --- /dev/null +++ b/apps/website/src/config/env.mjs @@ -0,0 +1,8 @@ +import { z } from 'zod' + +export const envSchema = z.object({ + INFURA_API_KEY: z.string(), + TAMAGUI_TARGET: z.literal('web'), +}) + +export const env = envSchema.parse(process.env) diff --git a/apps/website/src/consts/error-codes.ts b/apps/website/src/consts/error-codes.ts index 834df6e8..d86b3496 100644 --- a/apps/website/src/consts/error-codes.ts +++ b/apps/website/src/consts/error-codes.ts @@ -2,4 +2,5 @@ export const ERROR_CODES = { NOT_FOUND: 404, INTERNAL_SERVER_ERROR: 500, INVALID_PUBLIC_KEY: 601, + INVALID_ENS_NAME: 602, } diff --git a/apps/website/src/hooks/use-url-data.ts b/apps/website/src/hooks/use-url-data.ts index 296f2e10..eb24825e 100644 --- a/apps/website/src/hooks/use-url-data.ts +++ b/apps/website/src/hooks/use-url-data.ts @@ -13,6 +13,7 @@ import { import { ERROR_CODES } from '@/consts/error-codes' import type { Data } from '@/components/preview-page' +import type { EnsResponse } from '@/pages/api/ens' import type { ChannelInfo, CommunityInfo, UserInfo } from '@status-im/js' import type { decodeChannelURLData, @@ -34,6 +35,7 @@ export const useURLData = ( const [channelUuid, setChannelUuid] = useState() const [data, setData] = useState() const [error, setError] = useState() + const [isLoading, setIsLoading] = useState(false) const compressPublicKey = type !== 'profile' @@ -47,7 +49,7 @@ export const useURLData = ( const hash = window.location.hash.replace('#', '') - // use provided public key + // use provided public key or recover it from ENS name if (!decodedData || !encodedData) { if (!hash) { setError('NOT_FOUND') @@ -55,6 +57,32 @@ export const useURLData = ( return } + // recover public key from ENS name + const ensName = hash.match(/^.+\.eth$/)?.[0] + if (ensName) { + const fetchEnsPubkey = async () => { + try { + const response = await fetch('/api/ens', { + method: 'POST', + body: JSON.stringify({ ensName, compress: compressPublicKey }), + }) + const { publicKey } = (await response.json()) as EnsResponse + + setPublicKey(publicKey) + } catch { + setError('INVALID_ENS_NAME') + } + + setIsLoading(false) + } + + setIsLoading(true) + fetchEnsPubkey() + + return + } + + // use provided public key try { const publicKey = deserializePublicKey(hash, { compress: compressPublicKey, @@ -71,7 +99,7 @@ export const useURLData = ( } } - // recover public key + // recover public key from encoded data let deserializedPublicKey try { const recoveredPublicKey = recoverPublicKeyFromEncodedURLData( @@ -154,5 +182,6 @@ export const useURLData = ( channelUuid, data, errorCode: error ? ERROR_CODES[error] : undefined, + isLoading, } } diff --git a/apps/website/src/lib/ethereum-client.ts b/apps/website/src/lib/ethereum-client.ts new file mode 100644 index 00000000..ff6fc4e1 --- /dev/null +++ b/apps/website/src/lib/ethereum-client.ts @@ -0,0 +1,17 @@ +import { EthereumClient } from '@status-im/js' + +import { env } from '@/config/env.mjs' + +let client: EthereumClient | undefined + +export function getEthereumClient(): EthereumClient | undefined { + if (!client) { + client = new EthereumClient( + `https://mainnet.infura.io/v3/${env.INFURA_API_KEY}` + ) + + return client + } + + return client +} diff --git a/apps/website/src/pages/api/ens.ts b/apps/website/src/pages/api/ens.ts new file mode 100644 index 00000000..514d99dc --- /dev/null +++ b/apps/website/src/pages/api/ens.ts @@ -0,0 +1,32 @@ +import { getEthereumClient } from '@/lib/ethereum-client' + +import type { NextApiRequest, NextApiResponse } from 'next' + +export type EnsResponse = { + publicKey: string +} + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + const { ensName, compress } = JSON.parse(req.body) + + const client = getEthereumClient() + + if (!client) { + return + } + + const publicKey = await client.resolvePublicKey(ensName, { + compress, + }) + + if (!publicKey) { + res.status(404).end() + + return + } + + res.status(200).json({ publicKey }) +} diff --git a/apps/website/src/server/og.ts b/apps/website/src/server/og.ts deleted file mode 100644 index bffc96f4..00000000 --- a/apps/website/src/server/og.ts +++ /dev/null @@ -1,33 +0,0 @@ -// see https://vercel.com/docs/concepts/functions/edge-functions/og-image-generation - -import { ImageResponse } from '@vercel/og' - -import type { NextRequest } from 'next/server' - -export const config = { - runtime: 'edge', -} - -// todo?: set cache header too -export function createHandler( - createComponent: (url: URL) => React.ReactElement -) { - const handler = async (req: NextRequest) => { - try { - const component = createComponent(new URL(req.url)) - - return new ImageResponse(component, { - width: 1200, - height: 630, - }) - } catch (error) { - console.error(error) - - return new Response(`Failed to generate the image`, { - status: 500, - }) - } - } - - return handler -} diff --git a/apps/website/tsconfig.json b/apps/website/tsconfig.json index d32c364d..88af3f1b 100644 --- a/apps/website/tsconfig.json +++ b/apps/website/tsconfig.json @@ -19,6 +19,6 @@ ] } }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], - "exclude": ["node_modules", "next.config.js", "tailwind.config.js"] + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "env.d.ts"], + "exclude": ["node_modules", "next.config.mjs", "tailwind.config.js"] } diff --git a/packages/status-js/src/ethereum-client/ethereum-client.ts b/packages/status-js/src/ethereum-client/ethereum-client.ts index d99a70e5..0eea7d35 100644 --- a/packages/status-js/src/ethereum-client/ethereum-client.ts +++ b/packages/status-js/src/ethereum-client/ethereum-client.ts @@ -8,7 +8,10 @@ export class EthereumClient { this.provider = new ethers.JsonRpcProvider(url) } - async resolveChatKey(ensName: string): Promise { + async resolvePublicKey( + ensName: string, + options: { compress: boolean } + ): Promise { try { const resolver = await this.provider.getResolver(ensName) @@ -32,7 +35,7 @@ export class EthereumClient { const point = new Point(px, py) point.assertValidity() - const hex = point.toHex(true) + const hex = point.toHex(options.compress) return `0x${hex}` } catch { diff --git a/yarn.lock b/yarn.lock index ddfd3e9b..25f7eefe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7783,7 +7783,7 @@ integrity sha512-0QhaE5mhaQbFlip4MX7n1nwCX8gax6Da1LsP2fZ/BU6xW9zyEmV6NX7DPelDxq1rr2NiBJh30vx9RIp80YeA/A== dependencies: "@use-gesture/core" "10.2.26" - + "@vercel/og@^0.5.4": version "0.5.4" resolved "https://registry.yarnpkg.com/@vercel/og/-/og-0.5.4.tgz#71de6335b94d0032b325936337c0259d85b9de19" @@ -20123,6 +20123,11 @@ yoga-wasm-web@0.3.3, yoga-wasm-web@^0.3.3: resolved "https://registry.yarnpkg.com/yoga-wasm-web/-/yoga-wasm-web-0.3.3.tgz#eb8e9fcb18e5e651994732f19a220cb885d932ba" integrity sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA== +zod@^3.21.4: + version "3.21.4" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.21.4.tgz#10882231d992519f0a10b5dd58a38c9dabbb64db" + integrity sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw== + zustand@^4.3.3: version "4.3.6" resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.3.6.tgz#ce7804eb75361af0461a2d0536b65461ec5de86f"