UNPKG

bigblocks

Version:

Complete Bitcoin UI component library - authentication, social, wallet, market, inscriptions, and blockchain interactions for React

277 lines (276 loc) 11.2 kB
"use client"; import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import React from "react"; export class HandCashOAuthProvider { config; state; listeners = new Set(); constructor(config) { this.config = config; this.state = { isConnected: false, authToken: null, profile: null, error: null, isLoading: false, }; } // Generate OAuth URL for HandCash Connect getOAuthUrl() { const baseUrl = this.config.environment === "iae" ? "https://iae-app.handcash.io" : "https://app.handcash.io"; const redirectUrl = this.config.redirectUrl || (typeof window !== "undefined" ? `${window.location.origin}/auth/handcash/callback` : "/auth/handcash/callback"); const params = new globalThis.URLSearchParams({ app: this.config.appId, redirectUrl: redirectUrl, }); return `${baseUrl}/oauth/authorize?${params.toString()}`; } // Start OAuth flow async startOAuth() { this.setState({ isLoading: true, error: null }); try { // Store state for OAuth flow const oauthState = { timestamp: Date.now(), nonce: globalThis.crypto.getRandomValues(new Uint8Array(16)).toString(), }; if (typeof window !== "undefined") { sessionStorage.setItem("handcash-oauth-state", JSON.stringify(oauthState)); } // Redirect to HandCash if (typeof window !== "undefined") { window.location.href = this.getOAuthUrl(); } else { throw new Error("Cannot redirect to HandCash during server-side rendering"); } } catch (error) { this.setState({ isLoading: false, error: error instanceof Error ? error.message : "Failed to start OAuth flow", }); } } // Handle OAuth callback (call this when user returns from HandCash) async handleCallback(authToken) { if (!authToken) { this.setState({ error: "No auth token received", isLoading: false }); return false; } this.setState({ isLoading: true, error: null }); try { // Verify token and get profile const profile = await this.getProfile(authToken); this.setState({ isConnected: true, authToken, profile, isLoading: false, error: null, }); // Clear OAuth state if (typeof window !== "undefined") { sessionStorage.removeItem("handcash-oauth-state"); } return true; } catch (error) { this.setState({ isLoading: false, error: error instanceof Error ? error.message : "Failed to verify auth token", }); return false; } } // Get user profile from backend async getProfile(authToken) { const response = await fetch("/api/auth/handcash/profile", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ authToken }), }); if (!response.ok) { throw new Error("Failed to fetch profile"); } return await response.json(); } // Encrypt data using HandCash encryption async encryptData(data) { if (!this.state.authToken) { throw new Error("Not authenticated"); } const response = await fetch("/api/auth/handcash/encrypt", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ authToken: this.state.authToken, data, }), }); if (!response.ok) { throw new Error("Failed to encrypt data"); } const result = await response.json(); return result.encryptedData; } // Decrypt data using HandCash decryption async decryptData(encryptedData) { if (!this.state.authToken) { throw new Error("Not authenticated"); } const response = await fetch("/api/auth/handcash/decrypt", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ authToken: this.state.authToken, encryptedData, }), }); if (!response.ok) { throw new Error("Failed to decrypt data"); } const result = await response.json(); return JSON.parse(result.decryptedData); } // Disconnect from HandCash async disconnect() { this.setState({ isConnected: false, authToken: null, profile: null, error: null, isLoading: false, }); // Clear any stored tokens if (typeof window !== "undefined") { sessionStorage.removeItem("handcash-auth-token"); sessionStorage.removeItem("handcash-oauth-state"); } } // Get current state getState() { return { ...this.state }; } // Subscribe to state changes subscribe(listener) { this.listeners.add(listener); return () => this.listeners.delete(listener); } // Update state and notify listeners setState(updates) { this.state = { ...this.state, ...updates }; for (const listener of this.listeners) { listener(this.state); } } } // React hook for HandCash OAuth export function useHandCash(config) { const [provider] = React.useState(() => new HandCashOAuthProvider(config)); const [state, setState] = React.useState(provider.getState()); React.useEffect(() => { const unsubscribe = provider.subscribe(setState); return unsubscribe; }, [provider]); const startOAuth = React.useCallback(async () => { await provider.startOAuth(); }, [provider]); const handleCallback = React.useCallback(async (authToken) => { return await provider.handleCallback(authToken); }, [provider]); const encryptData = React.useCallback(async (data) => { return await provider.encryptData(data); }, [provider]); const decryptData = React.useCallback(async (encryptedData) => { return await provider.decryptData(encryptedData); }, [provider]); const disconnect = React.useCallback(async () => { await provider.disconnect(); }, [provider]); return { ...state, startOAuth, handleCallback, encryptData, decryptData, disconnect, provider, }; } // HandCash OAuth authentication handler // Integrates with OAuth system like Google/GitHub providers export async function createHandCashAuthHandler(config) { try { // Check if we have an authorization code in the config (OAuth callback) if (config.code) { // This is a callback - let backend handle the token exchange and OAuth integration // Backend will: // 1. Exchange authorization code for HandCash access token // 2. Get HandCash profile data // 3. Use verifyByAddress API to lookup BAP ID from paymail/address // 4. Create OAuth session token for our system // 5. Retrieve encrypted backup if exists const response = await fetch("/api/auth/handcash/oauth", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ authorizationCode: config.code, provider: "handcash", // OAuth provider identifier state: config.state, // Include state for CSRF protection // Don't send appSecret! Backend should have it securely stored }), }); if (!response.ok) { return { success: false, error: "Failed to complete HandCash OAuth authentication", }; } const result = await response.json(); return { success: true, authToken: result.authToken, // HandCash access token oauthToken: result.oauthToken, // Our OAuth system token idKey: result.idKey, // Backend uses verifyByAddress API to lookup BAP ID encryptedBackup: result.encryptedBackup, profile: { handle: result.profile.handle, displayName: result.profile.displayName, paymail: result.profile.paymail, avatarUrl: result.profile.avatarUrl, address: result.profile.address, }, }; } // For HandCash, we need to handle this differently since we don't have the config here // This would normally redirect to HandCash OAuth URL return { success: false, error: "HandCash OAuth flow must be initiated from the frontend", }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : "HandCash authentication failed", }; } } export function HandCashConnectPrompt({ onConnect, className = "", config, }) { const { startOAuth, isLoading, error } = useHandCash(config); const handleConnect = async () => { await startOAuth(); onConnect?.(); }; return (_jsx("div", { className: `text-center space-y-4 ${className}`, children: _jsxs("div", { className: "p-4 bg-green-900/20 border border-green-500/30 rounded-lg", children: [_jsx("div", { className: "w-12 h-12 bg-green-600 rounded-full flex items-center justify-center mx-auto mb-3", children: _jsxs("svg", { className: "w-6 h-6", viewBox: "0 0 24 24", children: [_jsx("title", { children: "HandCash" }), _jsx("path", { fill: "white", d: "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1.5 15h-3v-2h3v2zm0-3h-3V7h3v7z" })] }) }), _jsx("h3", { className: "text-lg font-semibold text-green-300 mb-2", children: "Connect with HandCash" }), _jsx("p", { className: "text-gray-300 text-sm mb-4", children: "Connect your HandCash wallet to authenticate and manage your Bitcoin identity." }), error && (_jsx("div", { className: "mb-4 p-3 bg-red-900/20 border border-red-500/30 rounded text-red-300 text-sm", children: error })), _jsx("button", { type: "button", onClick: handleConnect, disabled: isLoading, className: "w-full px-4 py-2 bg-green-600 hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed text-white rounded-lg transition-colors", children: isLoading ? "Connecting..." : "Connect HandCash Wallet" })] }) })); }