@stablecoin.xyz/react
Version:
React hooks and components for SBC Account Abstraction
408 lines (399 loc) • 18.3 kB
JavaScript
'use strict';
var react = require('react');
var jsxRuntime = require('react/jsx-runtime');
var core = require('@stablecoin.xyz/core');
const SbcContext = react.createContext(undefined);
function SbcProvider({ config, children, onError }) {
const [sbcAppKit, setSbcAppKit] = react.useState(null);
const [isInitialized, setIsInitialized] = react.useState(false);
const [error, setError] = react.useState(null);
const initialize = react.useCallback(async () => {
try {
setError(null);
setIsInitialized(false);
const appKit = new core.SbcAppKit(config);
setSbcAppKit(appKit);
setIsInitialized(true);
}
catch (err) {
const error = err instanceof Error ? err : new Error('Failed to initialize SBC App Kit');
setError(error);
setSbcAppKit(null);
setIsInitialized(false);
onError?.(error);
}
}, [config, onError]);
// Initialize on mount and config changes
react.useEffect(() => {
initialize();
}, [initialize]);
// Cleanup on unmount
react.useEffect(() => {
return () => {
};
}, [sbcAppKit]);
const contextValue = {
sbcAppKit,
isInitialized,
error,
};
return (jsxRuntime.jsx(SbcContext.Provider, { value: contextValue, children: children }));
}
function useSbcContext() {
const context = react.useContext(SbcContext);
if (context === undefined) {
throw new Error('useSbcContext must be used within a SbcProvider');
}
return context;
}
/**
* Main hook for accessing SBC App Kit functionality
*/
function useSbcApp() {
const { sbcAppKit, isInitialized, error } = useSbcContext();
const [account, setAccount] = react.useState(null);
const [isLoadingAccount, setIsLoadingAccount] = react.useState(false);
const [accountError, setAccountError] = react.useState(null);
const [ownerAddress, setOwnerAddress] = react.useState(null);
const refreshAccount = react.useCallback(async () => {
if (!sbcAppKit || !isInitialized) {
setAccount(null);
setOwnerAddress(null);
return;
}
try {
setIsLoadingAccount(true);
setAccountError(null);
// First try to get owner address to check if wallet is connected
try {
const owner = sbcAppKit.getOwnerAddress();
setOwnerAddress(owner);
// Only try to get account if wallet is connected
const accountInfo = await sbcAppKit.getAccount();
setAccount(accountInfo);
}
catch (ownerError) {
// No wallet connected yet - this is normal for 'auto' wallet config
setOwnerAddress(null);
setAccount(null);
// Don't set this as an error - it's expected behavior
return;
}
}
catch (err) {
const error = err instanceof Error ? err : new Error('Failed to load account');
setAccountError(error);
setAccount(null);
setOwnerAddress(null);
}
finally {
setIsLoadingAccount(false);
}
}, [sbcAppKit, isInitialized]);
// Disconnect wallet and clear state
const disconnectWallet = react.useCallback(() => {
if (sbcAppKit) {
try {
sbcAppKit.disconnectWallet();
}
catch { }
}
setAccount(null);
setOwnerAddress(null);
}, [sbcAppKit]);
// Load account when SDK is initialized
react.useEffect(() => {
if (isInitialized && sbcAppKit) {
refreshAccount();
}
else {
setAccount(null);
setAccountError(null);
setOwnerAddress(null);
}
}, [isInitialized, sbcAppKit, refreshAccount]);
return {
sbcAppKit,
isInitialized,
error,
account,
isLoadingAccount,
accountError,
refreshAccount,
ownerAddress,
disconnectWallet,
};
}
/**
* Hook for sending user operations with automatic state management
*/
function useUserOperation(options = {}) {
const { sbcAppKit, isInitialized } = useSbcContext();
const { refreshAccount } = useSbcApp();
const { onSuccess, onError, refreshAccount: shouldRefreshAccount = true } = options;
const [isLoading, setIsLoading] = react.useState(false);
const [isSuccess, setIsSuccess] = react.useState(false);
const [isError, setIsError] = react.useState(false);
const [error, setError] = react.useState(null);
const [data, setData] = react.useState(null);
const reset = react.useCallback(() => {
setIsLoading(false);
setIsSuccess(false);
setIsError(false);
setError(null);
setData(null);
}, []);
const sendUserOperation = react.useCallback(async (params) => {
if (!sbcAppKit || !isInitialized) {
const error = new Error('SBC App Kit is not initialized');
setError(error);
setIsError(true);
onError?.(error);
return;
}
try {
setIsLoading(true);
setIsError(false);
setError(null);
setIsSuccess(false);
const result = await sbcAppKit.sendUserOperation(params);
setData(result);
setIsSuccess(true);
onSuccess?.(result);
// Auto-refresh account if enabled
if (shouldRefreshAccount) {
refreshAccount();
}
return result;
}
catch (err) {
const error = err instanceof Error ? err : new Error('Failed to send user operation');
setError(error);
setIsError(true);
setData(null);
onError?.(error);
return;
}
finally {
setIsLoading(false);
}
}, [sbcAppKit, isInitialized, onSuccess, onError, shouldRefreshAccount, refreshAccount]);
const estimateUserOperation = react.useCallback(async (params) => {
if (!sbcAppKit || !isInitialized) {
const error = new Error('SBC App Kit is not initialized');
setError(error);
setIsError(true);
onError?.(error);
return;
}
try {
return await sbcAppKit.estimateUserOperation(params);
}
catch (err) {
const error = err instanceof Error ? err : new Error('Failed to estimate user operation');
setError(error);
setIsError(true);
onError?.(error);
return;
}
}, [sbcAppKit, isInitialized, onError]);
return {
sendUserOperation,
estimateUserOperation,
isLoading,
isSuccess,
isError,
error,
data,
reset,
};
}
/**
* WalletConnect component - placeholder for future wallet integration
*
* This component will be expanded in the future to handle:
* - Multiple wallet providers (MetaMask, WalletConnect, Coinbase Wallet, etc.)
* - Wallet switching
* - Connection state management
* - Network switching
*/
function WalletConnect({ className, onConnectionChange }) {
return (jsxRuntime.jsx("div", { className: className, children: jsxRuntime.jsxs("div", { style: {
padding: '12px 16px',
border: '1px solid #e0e0e0',
borderRadius: '8px',
textAlign: 'center',
backgroundColor: '#f8f9fa'
}, children: [jsxRuntime.jsx("p", { style: { margin: '0 0 8px 0', fontSize: '14px', color: '#666' }, children: "\uD83D\uDEA7 Wallet Connection Coming Soon" }), jsxRuntime.jsx("p", { style: { margin: 0, fontSize: '12px', color: '#999' }, children: "This component will support multiple wallet providers and connection management." })] }) }));
}
/**
* WalletButton - Simple button component for connecting to wallets
*
* Automatically detects available wallets and connects with one click
*/
function WalletButton({ walletType = 'auto', className = '', onConnect, onError, children, showLoading = true, disabled = false, render, }) {
const { sbcAppKit, refreshAccount } = useSbcApp();
const [isConnecting, setIsConnecting] = react.useState(false);
const [error, setError] = react.useState(null);
const handleConnect = async () => {
if (!sbcAppKit || isConnecting || disabled)
return;
try {
setIsConnecting(true);
setError(null);
const result = await sbcAppKit.connectWallet(walletType);
// Refresh app state after connection
await refreshAccount();
onConnect?.(result);
}
catch (err) {
const error = err instanceof Error ? err : new Error('Failed to connect wallet');
setError(error.message);
onError?.(error);
}
finally {
setIsConnecting(false);
}
};
const getButtonText = () => {
if (children)
return children;
if (isConnecting)
return 'Connecting...';
if (walletType === 'metamask')
return 'Connect MetaMask';
if (walletType === 'coinbase')
return 'Connect Coinbase Wallet';
if (walletType === 'walletconnect')
return 'Connect WalletConnect';
return 'Connect Wallet';
};
const baseStyles = `
px-4 py-2 rounded-md font-medium transition-colors duration-200
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2
disabled:opacity-50 disabled:cursor-not-allowed
`.trim();
const stateStyles = isConnecting || disabled
? 'bg-gray-300 text-gray-600 cursor-not-allowed'
: 'bg-blue-600 text-white hover:bg-blue-700 active:bg-blue-800';
const finalClassName = `${baseStyles} ${stateStyles} ${className}`.trim();
// If render prop is provided, use it for rendering
if (render) {
return (jsxRuntime.jsxs("div", { className: "space-y-2", children: [render({
onClick: handleConnect,
isConnecting,
disabled: isConnecting || disabled || !sbcAppKit,
children: getButtonText(),
className: finalClassName,
}), error && (jsxRuntime.jsx("div", { className: "text-sm text-red-600 bg-red-50 border border-red-200 rounded px-3 py-2", children: error }))] }));
}
return (jsxRuntime.jsxs("div", { className: "space-y-2", children: [jsxRuntime.jsxs("button", { type: "button", className: finalClassName, onClick: handleConnect, disabled: isConnecting || disabled || !sbcAppKit, children: [showLoading && isConnecting && (jsxRuntime.jsx("span", { className: "inline-block w-4 h-4 mr-2 animate-spin rounded-full border-2 border-transparent border-t-current" })), getButtonText()] }), error && (jsxRuntime.jsx("div", { className: "text-sm text-red-600 bg-red-50 border border-red-200 rounded px-3 py-2", children: error }))] }));
}
/**
* WalletSelector - Component that displays available wallets and allows selection
*
* Automatically detects installed wallets and shows connection options
*/
function WalletSelector({ className = '', onConnect, onError, showOnlyAvailable = true, wallets: customWallets, }) {
const { sbcAppKit } = useSbcContext();
const [availableWallets, setAvailableWallets] = react.useState([]);
const [isLoading, setIsLoading] = react.useState(true);
const [connectingWallet, setConnectingWallet] = react.useState(null);
// Load available wallets on mount
react.useEffect(() => {
const loadWallets = async () => {
if (!sbcAppKit)
return;
try {
setIsLoading(true);
// TODO: Replace with sbcAppKit.getAvailableWallets() once new version is published
if (sbcAppKit.getAvailableWallets) {
const wallets = await sbcAppKit.getAvailableWallets();
setAvailableWallets(wallets);
}
else {
// Fallback: Use hardcoded wallet list
const fallbackWallets = [
{
type: 'metamask',
name: 'MetaMask',
available: typeof window !== 'undefined' && !!window.ethereum,
icon: 'https://docs.metamask.io/img/metamask-fox.svg',
},
{
type: 'coinbase',
name: 'Coinbase Wallet',
available: typeof window !== 'undefined' && !!(window.ethereum || window.coinbaseWalletExtension),
icon: 'https://wallet-api-production.s3.amazonaws.com/uploads/tokens/eth_288.png',
},
{
type: 'walletconnect',
name: 'WalletConnect',
available: true,
icon: 'https://registry.walletconnect.com/api/v1/logo/sm/walletconnect.png',
},
];
setAvailableWallets(fallbackWallets);
}
}
catch (error) {
console.error('Failed to load available wallets:', error);
setAvailableWallets([]);
}
finally {
setIsLoading(false);
}
};
loadWallets();
}, [sbcAppKit]);
const handleWalletConnect = async (walletType) => {
if (!sbcAppKit || connectingWallet)
return;
try {
setConnectingWallet(walletType);
// TODO: Replace with sbcAppKit.connectWallet() once new version is published
if (sbcAppKit.connectWallet) {
const result = await sbcAppKit.connectWallet(walletType);
onConnect?.(result);
}
else {
throw new Error('Wallet integration coming soon! Use the dev:local script to test with the latest SDK features.');
}
}
catch (err) {
const error = err instanceof Error ? err : new Error('Failed to connect wallet');
onError?.(error);
}
finally {
setConnectingWallet(null);
}
};
// Use custom wallets if provided, otherwise use detected wallets
const walletsToShow = customWallets || availableWallets;
const filteredWallets = showOnlyAvailable
? walletsToShow.filter(w => w.available)
: walletsToShow;
if (isLoading) {
return (jsxRuntime.jsx("div", { className: `space-y-4 ${className}`, children: jsxRuntime.jsxs("div", { className: "text-center text-gray-600", children: [jsxRuntime.jsx("div", { className: "inline-block w-6 h-6 animate-spin rounded-full border-2 border-gray-300 border-t-blue-600" }), jsxRuntime.jsx("p", { className: "mt-2", children: "Detecting available wallets..." })] }) }));
}
if (filteredWallets.length === 0) {
return (jsxRuntime.jsxs("div", { className: `text-center p-6 bg-gray-50 border border-gray-200 rounded-lg ${className}`, children: [jsxRuntime.jsx("p", { className: "text-gray-600 mb-4", children: "No compatible wallets found." }), jsxRuntime.jsx("p", { className: "text-sm text-gray-500", children: "Please install MetaMask, Coinbase Wallet, or use WalletConnect to continue." })] }));
}
return (jsxRuntime.jsxs("div", { className: `space-y-3 ${className}`, children: [jsxRuntime.jsx("h3", { className: "text-lg font-medium text-gray-900 mb-4", children: "Connect Wallet" }), filteredWallets.map((wallet) => (jsxRuntime.jsxs("button", { type: "button", className: `
w-full flex items-center justify-between p-4 border rounded-lg transition-colors
${wallet.available
? 'border-gray-200 hover:border-blue-300 hover:bg-blue-50 focus:border-blue-500 focus:ring-2 focus:ring-blue-200'
: 'border-gray-100 bg-gray-50 cursor-not-allowed opacity-60'}
${connectingWallet === wallet.type ? 'border-blue-500 bg-blue-50' : ''}
`, onClick: () => handleWalletConnect(wallet.type), disabled: !wallet.available || connectingWallet !== null, children: [jsxRuntime.jsxs("div", { className: "flex items-center space-x-3", children: [wallet.icon && (jsxRuntime.jsx("img", { src: wallet.icon, alt: `${wallet.name} icon`, className: "w-8 h-8 rounded", onError: (e) => {
// Hide broken images
e.target.style.display = 'none';
} })), jsxRuntime.jsxs("div", { className: "text-left", children: [jsxRuntime.jsx("p", { className: "font-medium text-gray-900", children: wallet.name }), jsxRuntime.jsx("p", { className: "text-sm text-gray-500", children: wallet.available ? 'Ready to connect' : 'Not installed' })] })] }), jsxRuntime.jsx("div", { className: "flex items-center", children: connectingWallet === wallet.type ? (jsxRuntime.jsx("div", { className: "w-5 h-5 animate-spin rounded-full border-2 border-blue-600 border-t-transparent" })) : wallet.available ? (jsxRuntime.jsx("svg", { className: "w-5 h-5 text-gray-400", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M9 5l7 7-7 7" }) })) : (jsxRuntime.jsx("svg", { className: "w-5 h-5 text-gray-300", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) })) })] }, wallet.type)))] }));
}
exports.SbcProvider = SbcProvider;
exports.WalletButton = WalletButton;
exports.WalletConnect = WalletConnect;
exports.WalletSelector = WalletSelector;
exports.useSbcApp = useSbcApp;
exports.useSbcContext = useSbcContext;
exports.useUserOperation = useUserOperation;