bigblocks
Version:
Complete Bitcoin UI component library - authentication, social, wallet, market, inscriptions, and blockchain interactions for React
17 lines • 8.88 kB
JSON
{
"name": "wallet-integrations-handcashconnector",
"type": "registry:component",
"dependencies": [
"@radix-ui/react-icons"
],
"devDependencies": [],
"registryDependencies": [],
"files": [
{
"path": "components/wallet-integrations/HandCashConnector.tsx",
"type": "registry:component",
"content": "/**\n * HandCash Connector Component\n *\n * Provides a complete UI for connecting to HandCash with:\n * - OAuth flow management\n * - Callback handling\n * - Error handling\n * - Loading states\n */\n\nimport { PersonIcon } from \"@radix-ui/react-icons\";\nimport React from \"react\";\nimport type {\n\tHandCashConfig,\n\tHandCashProfile,\n} from \"../../lib/handcash-provider.js\";\nimport { cn } from \"../../lib/utils.js\";\nimport { HandCashIcon } from \"../ui-components/HandCashIcon.js\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"../ui/avatar.js\";\nimport { Button } from \"../ui/button.js\";\nimport { Card, CardContent } from \"../ui/card.js\";\nimport { OAUTH_BRAND_COLORS } from \"./OAuthProviders.js\";\n\ninterface HandCashConnectorProps {\n\tconfig: HandCashConfig;\n\tonSuccess?: (result: {\n\t\tauthToken: string;\n\t\tprofile: HandCashProfile;\n\t\tidKey?: string;\n\t\tencryptedBackup?: string;\n\t}) => void;\n\tonError?: (error: string) => void;\n\tclassName?: string;\n}\n\ninterface HandCashState {\n\tisLoading: boolean;\n\tisConnected: boolean;\n\tprofile: HandCashProfile | null;\n\tauthToken: string | null;\n\terror: string | null;\n}\n\nexport function HandCashConnector({\n\tconfig,\n\tonSuccess,\n\tonError,\n\tclassName = \"\",\n}: HandCashConnectorProps) {\n\tconst [state, setState] = React.useState<HandCashState>({\n\t\tisLoading: false,\n\t\tisConnected: false,\n\t\tprofile: null,\n\t\tauthToken: null,\n\t\terror: null,\n\t});\n\n\t// Lazy load the HandCash integration\n\tconst [handCashModule, setHandCashModule] = React.useState<\n\t\ttypeof import(\"../../lib/handcash-provider.js\") | null\n\t>(null);\n\n\tconst loadHandCashModule = React.useCallback(async () => {\n\t\ttry {\n\t\t\tconst module = await import(\"../../lib/handcash-provider.js\");\n\t\t\tsetHandCashModule(module);\n\t\t} catch {\n\t\t\tsetState((prev) => ({\n\t\t\t\t...prev,\n\t\t\t\terror: \"Failed to load HandCash integration\",\n\t\t\t}));\n\t\t}\n\t}, []);\n\n\tconst handleOAuthCallback = React.useCallback(async () => {\n\t\t// Check URL for HandCash callback\n\t\tconst urlParams = new globalThis.URLSearchParams(window.location.search);\n\t\tconst authToken = urlParams.get(\"authToken\");\n\n\t\tif (authToken && handCashModule) {\n\t\t\tsetState((prev) => ({ ...prev, isLoading: true }));\n\n\t\t\ttry {\n\t\t\t\tconst provider = new handCashModule.HandCashOAuthProvider(config);\n\t\t\t\tconst success = await provider.handleCallback(authToken);\n\n\t\t\t\tif (success) {\n\t\t\t\t\tconst providerState = provider.getState();\n\t\t\t\t\tsetState((prev) => ({\n\t\t\t\t\t\t...prev,\n\t\t\t\t\t\tisConnected: true,\n\t\t\t\t\t\tprofile: providerState.profile,\n\t\t\t\t\t\tauthToken: providerState.authToken,\n\t\t\t\t\t\tisLoading: false,\n\t\t\t\t\t\terror: null,\n\t\t\t\t\t}));\n\n\t\t\t\t\t// Clean up URL\n\t\t\t\t\twindow.history.replaceState(\n\t\t\t\t\t\t{},\n\t\t\t\t\t\tdocument.title,\n\t\t\t\t\t\twindow.location.pathname,\n\t\t\t\t\t);\n\n\t\t\t\t\tonSuccess?.({\n\t\t\t\t\t\tauthToken: providerState.authToken ?? \"\",\n\t\t\t\t\t\tprofile: providerState.profile ?? ({} as HandCashProfile),\n\t\t\t\t\t\t// These would come from your backend after processing\n\t\t\t\t\t\tidKey: providerState.profile?.id,\n\t\t\t\t\t\tencryptedBackup: undefined,\n\t\t\t\t\t});\n\t\t\t\t} else {\n\t\t\t\t\tconst errorMsg = provider.getState().error || \"Authentication failed\";\n\t\t\t\t\tsetState((prev) => ({\n\t\t\t\t\t\t...prev,\n\t\t\t\t\t\tisLoading: false,\n\t\t\t\t\t\terror: errorMsg,\n\t\t\t\t\t}));\n\t\t\t\t\tonError?.(errorMsg);\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tconst errorMsg =\n\t\t\t\t\terror instanceof Error ? error.message : \"Unknown error occurred\";\n\t\t\t\tsetState((prev) => ({\n\t\t\t\t\t...prev,\n\t\t\t\t\tisLoading: false,\n\t\t\t\t\terror: errorMsg,\n\t\t\t\t}));\n\t\t\t\tonError?.(errorMsg);\n\t\t\t}\n\t\t}\n\t}, [handCashModule, config, onSuccess, onError]);\n\n\tReact.useEffect(() => {\n\t\t// Load HandCash module\n\t\tloadHandCashModule();\n\n\t\t// Check if this is a callback from HandCash\n\t\thandleOAuthCallback();\n\t}, [handleOAuthCallback, loadHandCashModule]);\n\n\tconst handleConnect = async () => {\n\t\tif (!handCashModule) {\n\t\t\tsetState((prev) => ({ ...prev, error: \"HandCash module not loaded\" }));\n\t\t\treturn;\n\t\t}\n\n\t\tsetState((prev) => ({ ...prev, isLoading: true, error: null }));\n\n\t\ttry {\n\t\t\tconst provider = new handCashModule.HandCashOAuthProvider(config);\n\t\t\tawait provider.startOAuth();\n\t\t\t// Page will redirect, so this won't continue\n\t\t} catch (error) {\n\t\t\tconst errorMsg =\n\t\t\t\terror instanceof Error ? error.message : \"Failed to start OAuth flow\";\n\t\t\tsetState((prev) => ({\n\t\t\t\t...prev,\n\t\t\t\tisLoading: false,\n\t\t\t\terror: errorMsg,\n\t\t\t}));\n\t\t\tonError?.(errorMsg);\n\t\t}\n\t};\n\n\tconst handleDisconnect = async () => {\n\t\tif (!handCashModule) return;\n\n\t\ttry {\n\t\t\tconst provider = new handCashModule.HandCashOAuthProvider(config);\n\t\t\tawait provider.disconnect();\n\t\t\tsetState({\n\t\t\t\tisLoading: false,\n\t\t\t\tisConnected: false,\n\t\t\t\tprofile: null,\n\t\t\t\tauthToken: null,\n\t\t\t\terror: null,\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tconsole.error(\"Error disconnecting:\", error);\n\t\t}\n\t};\n\n\t// Loading state\n\tif (state.isLoading) {\n\t\treturn (\n\t\t\t<div className={cn(\"flex flex-col items-center gap-4\", className)}>\n\t\t\t\t<div className=\"animate-spin h-8 w-8 border-4 border-primary border-t-transparent rounded-full\" />\n\t\t\t\t<p className=\"text-muted-foreground\">Connecting to HandCash...</p>\n\t\t\t</div>\n\t\t);\n\t}\n\n\t// Connected state\n\tif (state.isConnected && state.profile) {\n\t\treturn (\n\t\t\t<div className={cn(\"flex flex-col items-center gap-4\", className)}>\n\t\t\t\t<Card className=\"w-full\">\n\t\t\t\t\t<CardContent className=\"flex flex-col items-center gap-3 p-6\">\n\t\t\t\t\t\t<Avatar className=\"h-20 w-20\">\n\t\t\t\t\t\t\t<AvatarImage src={state.profile.avatarUrl} />\n\t\t\t\t\t\t\t<AvatarFallback className=\"bg-primary\">\n\t\t\t\t\t\t\t\t<PersonIcon className=\"h-6 w-6\" />\n\t\t\t\t\t\t\t</AvatarFallback>\n\t\t\t\t\t\t</Avatar>\n\t\t\t\t\t\t<h2 className=\"text-2xl font-bold\">Connected to HandCash</h2>\n\t\t\t\t\t\t<div className=\"flex flex-col items-center gap-1\">\n\t\t\t\t\t\t\t<p className=\"text-base text-muted-foreground\">\n\t\t\t\t\t\t\t\t{state.profile.displayName}\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t<p className=\"text-sm text-muted-foreground\">\n\t\t\t\t\t\t\t\t@{state.profile.handle}\n\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\tonClick={handleDisconnect}\n\t\t\t\t\t\t\tsize=\"sm\"\n\t\t\t\t\t\t\tvariant=\"secondary\"\n\t\t\t\t\t\t\tclassName=\"mt-2\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\tDisconnect\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t</CardContent>\n\t\t\t\t</Card>\n\t\t\t</div>\n\t\t);\n\t}\n\n\t// Ready to connect state\n\treturn (\n\t\t<div className={cn(\"flex flex-col items-center gap-4\", className)}>\n\t\t\t<Card className=\"w-full\">\n\t\t\t\t<CardContent className=\"flex flex-col items-center gap-4 p-6\">\n\t\t\t\t\t<div\n\t\t\t\t\t\tclassName=\"w-12 h-12 rounded-full flex items-center justify-center\"\n\t\t\t\t\t\tstyle={{ backgroundColor: OAUTH_BRAND_COLORS.handcash }}\n\t\t\t\t\t>\n\t\t\t\t\t\t<HandCashIcon size={24} color=\"white\" />\n\t\t\t\t\t</div>\n\t\t\t\t\t<h2 className=\"text-2xl font-bold\">Connect with HandCash</h2>\n\t\t\t\t\t<p className=\"text-sm text-muted-foreground text-center\">\n\t\t\t\t\t\tConnect your HandCash wallet to authenticate with your Bitcoin\n\t\t\t\t\t\tidentity.\n\t\t\t\t\t</p>\n\n\t\t\t\t\t{state.error && (\n\t\t\t\t\t\t<Card className=\"w-full border-destructive\">\n\t\t\t\t\t\t\t<CardContent className=\"p-3\">\n\t\t\t\t\t\t\t\t<p className=\"text-sm text-destructive\">{state.error}</p>\n\t\t\t\t\t\t\t</CardContent>\n\t\t\t\t\t\t</Card>\n\t\t\t\t\t)}\n\n\t\t\t\t\t<Button\n\t\t\t\t\t\tonClick={handleConnect}\n\t\t\t\t\t\tdisabled={state.isLoading || !handCashModule}\n\t\t\t\t\t\tsize=\"lg\"\n\t\t\t\t\t\tclassName=\"w-full\"\n\t\t\t\t\t>\n\t\t\t\t\t\t{!handCashModule ? \"Loading...\" : \"Connect HandCash Wallet\"}\n\t\t\t\t\t</Button>\n\n\t\t\t\t\t<p className=\"text-xs text-muted-foreground text-center\">\n\t\t\t\t\t\tYou'll be redirected to HandCash to authorize the connection\n\t\t\t\t\t</p>\n\t\t\t\t</CardContent>\n\t\t\t</Card>\n\t\t</div>\n\t);\n}\n",
"target": "<%- config.aliases.components %>/handcashconnector.tsx"
}
]
}