UNPKG

@worldcoin/minikit-react

Version:

minikit-react is a set of hooks for building mini-apps

434 lines (429 loc) 12.1 kB
// src/address-book/is-verified.tsx import { getIsUserVerified } from "@worldcoin/minikit-js/address-book"; import { useEffect, useState } from "react"; var useIsUserVerified = (walletAddress, rpcUrl) => { const [isUserVerified, setIsUserVerified] = useState(null); const [isLoading, setIsLoading] = useState(true); const [isError, setIsError] = useState(null); useEffect(() => { const fetchIsUserVerified = async () => { try { const data = await getIsUserVerified(walletAddress); setIsUserVerified(data); } catch (err) { setIsError(err); } finally { setIsLoading(false); } }; fetchIsUserVerified(); }, [walletAddress]); return { isUserVerified, isLoading, isError }; }; // src/components/username-search.tsx import { jsx } from "react/jsx-runtime"; var createDebounce = () => { let timeoutId; return (fn, delay2) => { return (...args) => { if (timeoutId) { clearTimeout(timeoutId); } timeoutId = setTimeout(() => { fn(...args); }, delay2); }; }; }; var DEBOUNCE_DELAY_MS = 300; var debounce = createDebounce(); var getSearchedUsername = async (username) => { const response = await fetch( `https://usernames.worldcoin.org/api/v1/search/${username}` ); if (response.status === 200) { const json = await response.json(); return { status: response.status, data: json }; } return { status: response.status, error: "Error fetching data" }; }; var UsernameSearch = ({ value, handleChange, setSearchedUsernames, className, inputProps }) => { const debouncedSearch = debounce( async (e) => { const username = e.target.value; const data = await getSearchedUsername(username); setSearchedUsernames(data); }, DEBOUNCE_DELAY_MS ); const onChange = (e) => { debouncedSearch(e); handleChange(e); }; return /* @__PURE__ */ jsx( "input", { type: "text", value, onChange, className: className || "rounded-md border-black border-2", ...inputProps } ); }; // src/transaction/hooks.ts import { useCallback, useEffect as useEffect2, useMemo, useRef, useState as useState2 } from "react"; // src/transaction/index.ts var DEFAULT_API_BASE_URL = "https://developer.world.org"; function resolveMiniKitApiBaseUrl(config = {}) { if (config.apiBaseUrl) { return config.apiBaseUrl.replace(/\/$/, ""); } return DEFAULT_API_BASE_URL; } async function fetchJson(url, errorPrefix) { const response = await fetch(url, { method: "GET" }); if (!response.ok) { const error = await response.json(); throw new Error(`${errorPrefix}: ${error.message}`); } return await response.json(); } function normalizeUserOperationStatus(data) { return { userOpHash: data.userOpHash, transactionHash: data.transaction_hash ?? void 0, transactionStatus: data.status === "success" ? "mined" : data.status === "failed" ? "failed" : "pending", ...data.sender ? { sender: data.sender } : {}, ...data.nonce ? { nonce: data.nonce } : {} }; } async function fetchTransactionHash(appConfig, transactionId) { try { const baseUrl = resolveMiniKitApiBaseUrl(); return await fetchJson( `${baseUrl}/api/v2/minikit/transaction/${transactionId}?app_id=${appConfig.app_id}&type=transaction`, "Failed to fetch transaction status" ); } catch (error) { console.log("Error fetching transaction status", error); throw error instanceof Error ? error : new Error("Failed to fetch transaction status"); } } async function fetchUserOperationStatus(config, userOpHash) { try { const baseUrl = resolveMiniKitApiBaseUrl(config); const data = await fetchJson( `${baseUrl}/api/v2/minikit/userop/${userOpHash}`, "Failed to fetch userOp status" ); return normalizeUserOperationStatus(data); } catch (error) { console.log("Error fetching userOp status", error); throw error instanceof Error ? error : new Error("Failed to fetch userOp status"); } } // src/transaction/hooks.ts var DEFAULT_POLLING_INTERVAL_MS = 1e3; var USER_OPERATION_INITIAL_POLLING_INTERVAL_MS = 300; var USER_OPERATION_MAX_POLLING_INTERVAL_MS = 5e3; function createFixedPollingStrategy(intervalMs) { return { getPollDelayMs: () => intervalMs }; } function createExponentialBackoffStrategy(initialDelayMs, maxDelayMs) { return { getPollDelayMs: (attempt) => Math.min(initialDelayMs * 2 ** attempt, maxDelayMs) }; } function useReceiptPolling(options) { const { client, id, fetchStatus, strategy, confirmations = 1, timeout } = options; const [transactionHash, setTransactionHash] = useState2(void 0); const [receipt, setReceipt] = useState2(); const [transactionStatus, setTransactionStatus] = useState2(); const [isLoading, setIsLoading] = useState2(false); const [isError, setIsError] = useState2(false); const [error, setError] = useState2(); const [retryKey, setRetryKey] = useState2(0); const resetState = useCallback(() => { setTransactionHash(void 0); setReceipt(void 0); setTransactionStatus(void 0); setIsLoading(false); setIsError(false); setError(void 0); }, []); const retrigger = useCallback(() => { resetState(); setRetryKey((value) => value + 1); }, [resetState]); useEffect2(() => { if (!id) { resetState(); return; } const abortController = new AbortController(); let timeoutId = null; let attempt = 0; setIsLoading(true); setIsError(false); setError(void 0); const waitForReceipt = async (hash) => { if (abortController.signal.aborted) return; try { const nextReceipt = await client.waitForTransactionReceipt({ hash, confirmations, timeout }); if (abortController.signal.aborted) return; setReceipt(nextReceipt); setIsLoading(false); } catch (nextError) { if (abortController.signal.aborted) return; setIsError(true); setError( nextError instanceof Error ? nextError : new Error(String(nextError)) ); setIsLoading(false); } }; const poll = async () => { if (abortController.signal.aborted) { return; } try { const nextStatus = await fetchStatus(); if (abortController.signal.aborted) return; setTransactionStatus(nextStatus); if (nextStatus.transactionStatus === "failed") { setIsError(true); setError(new Error("Transaction failed")); setIsLoading(false); return; } if (nextStatus.transactionHash) { setTransactionHash(nextStatus.transactionHash); await waitForReceipt(nextStatus.transactionHash); return; } timeoutId = setTimeout(poll, strategy.getPollDelayMs(attempt)); attempt += 1; } catch (nextError) { if (abortController.signal.aborted) return; setIsError(true); setError( nextError instanceof Error ? nextError : new Error(String(nextError)) ); setIsLoading(false); } }; poll(); return () => { abortController.abort(); if (timeoutId) { clearTimeout(timeoutId); } }; }, [ id, retryKey, fetchStatus, strategy, client, confirmations, timeout, resetState ]); return { transactionHash, receipt, isError, isLoading, isSuccess: receipt?.status === "success" || transactionStatus?.transactionStatus === "mined", error, retrigger }; } function useWaitForTransactionReceipt(options) { const { appConfig, pollingInterval = DEFAULT_POLLING_INTERVAL_MS, transactionId } = options; const fetchStatus = useCallback(async () => { return await fetchTransactionHash(appConfig, transactionId); }, [appConfig.app_id, transactionId]); const strategy = useMemo( () => createFixedPollingStrategy(pollingInterval), [pollingInterval] ); return useReceiptPolling({ ...options, id: transactionId, fetchStatus, strategy }); } function useWaitForUserOperationReceipt(options) { const { apiBaseUrl, pollingInterval = USER_OPERATION_INITIAL_POLLING_INTERVAL_MS, userOpHash } = options; const fetchStatus = useCallback(async () => { return await fetchUserOperationStatus({ apiBaseUrl }, userOpHash); }, [apiBaseUrl, userOpHash]); const strategy = useMemo( () => createExponentialBackoffStrategy( pollingInterval, USER_OPERATION_MAX_POLLING_INTERVAL_MS ), [pollingInterval] ); return useReceiptPolling({ ...options, id: userOpHash, fetchStatus, strategy }); } function delay(ms, signal) { return new Promise((resolve, reject) => { const timer = setTimeout(resolve, ms); signal.addEventListener( "abort", () => { clearTimeout(timer); reject(new DOMException("Aborted", "AbortError")); }, { once: true } ); }); } async function pollForReceipt(params) { const { fetchStatus, strategy, client, confirmations, timeout, signal } = params; let attempt = 0; const throwIfAborted = () => { if (signal.aborted) throw new DOMException("Aborted", "AbortError"); }; while (true) { throwIfAborted(); const status = await fetchStatus(); throwIfAborted(); if (status.transactionStatus === "failed") { throw new Error("Transaction failed"); } if (status.transactionHash) { const receipt = await client.waitForTransactionReceipt({ hash: status.transactionHash, confirmations, timeout }); throwIfAborted(); return { transactionHash: status.transactionHash, receipt }; } await delay(strategy.getPollDelayMs(attempt), signal); attempt += 1; } } function usePolling(options) { const { execute } = options; const [isLoading, setIsLoading] = useState2(false); const abortRef = useRef(null); const executeRef = useRef(execute); executeRef.current = execute; const reset = useCallback(() => { abortRef.current?.abort(); abortRef.current = null; setIsLoading(false); }, []); useEffect2(() => { return () => { abortRef.current?.abort(); }; }, []); const poll = useCallback((id) => { abortRef.current?.abort(); const controller = new AbortController(); abortRef.current = controller; setIsLoading(true); return executeRef.current(id, controller.signal).finally(() => { if (abortRef.current === controller) { setIsLoading(false); } }); }, []); return { poll, isLoading, reset }; } function useUserOperationReceipt(options) { const { client, apiBaseUrl, pollingInterval = USER_OPERATION_INITIAL_POLLING_INTERVAL_MS, confirmations = 1, timeout } = options; const strategy = useMemo( () => createExponentialBackoffStrategy( pollingInterval, USER_OPERATION_MAX_POLLING_INTERVAL_MS ), [pollingInterval] ); return usePolling({ execute: (userOpHash, signal) => pollForReceipt({ fetchStatus: () => fetchUserOperationStatus({ apiBaseUrl }, userOpHash), strategy, client, confirmations, timeout, signal }) }); } function useTransactionReceipt(options) { const { client, appConfig, pollingInterval = DEFAULT_POLLING_INTERVAL_MS, confirmations = 1, timeout } = options; const strategy = useMemo( () => createFixedPollingStrategy(pollingInterval), [pollingInterval] ); return usePolling({ execute: (transactionId, signal) => pollForReceipt({ fetchStatus: () => fetchTransactionHash(appConfig, transactionId), strategy, client, confirmations, timeout, signal }) }); } export { UsernameSearch, useIsUserVerified, useTransactionReceipt, useUserOperationReceipt, useWaitForTransactionReceipt, useWaitForUserOperationReceipt };