bigblocks
Version:
Complete Bitcoin UI component library - authentication, social, wallet, market, inscriptions, and blockchain interactions for React
277 lines (276 loc) • 11.2 kB
JavaScript
"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" })] }) }));
}