Add cursor and privy provider

This commit is contained in:
2025-03-05 22:40:24 +07:00
parent 9498696d6d
commit ead68573a5
14 changed files with 698 additions and 72 deletions

View File

@@ -2,6 +2,7 @@
using Managing.Application.Abstractions.Services;
using Managing.Domain.Accounts;
using Managing.Domain.Users;
using Microsoft.Extensions.Logging;
namespace Managing.Application.Users;
@@ -10,24 +11,28 @@ public class UserService : IUserService
private readonly IEvmManager _evmManager;
private readonly IUserRepository _userRepository;
private readonly IAccountService _accountService;
private readonly ILogger<UserService> _logger;
private string[] authorizedAddresses = [
private string[] authorizedAddresses =
[
"0x6781920674dA695aa5120d95D80c4B1788046806", // Macbook
"0xA2B43AFF0992a47838DF2e6099A8439981f0B717", // Phone
"0xAD4bcf258852e9d47E580798d312E1a52D59E721", // Razil
"0xAd6D6c80910096b40e45690506a9f1052e072dCB", // Teru
"0x309b9235edbe1C6f840816771c6C21aDa6c275EE", // Cowchain
"0x23AA99254cfaA2c374bE2bA5B55C68018cCdFCb3" // Local optiflex
"0x23AA99254cfaA2c374bE2bA5B55C68018cCdFCb3", // Local optiflex
"0x932167388dD9aad41149b3cA23eBD489E2E2DD78" // Embedded wallet
];
public UserService(
IEvmManager evmManager,
IUserRepository userRepository,
IAccountService accountService)
IAccountService accountService, ILogger<UserService> logger)
{
_evmManager = evmManager;
_userRepository = userRepository;
_accountService = accountService;
_logger = logger;
}
public async Task<User> Authenticate(string name, string address, string message, string signature)
@@ -35,10 +40,16 @@ public class UserService : IUserService
var recoveredAddress = _evmManager.VerifySignature(signature, message);
if (!authorizedAddresses.Contains(recoveredAddress))
{
_logger.LogWarning($"Address {recoveredAddress} not authorized");
throw new Exception("Address not authorized");
}
if (recoveredAddress == null || !recoveredAddress.Equals(address))
{
_logger.LogWarning($"Address {recoveredAddress} not corresponding");
throw new Exception("Address not corresponding");
}
// Check if account exist
var account = await _accountService.GetAccountByKey(recoveredAddress, true, false);
@@ -97,4 +108,4 @@ public class UserService : IUserService
user.Accounts = _accountService.GetAccountsByUser(user).ToList();
return user;
}
}
}

View File

@@ -4,3 +4,4 @@ VITE_WORKER_URL_LOCAL=https://localhost:5002
VITE_WORKER_URL_SERVER=https://dev-managing-worker.apps.managing.live
ALCHEMY_ID=Bao7OirVe4bmYiDbPh0l8cs5gYb5D4_9
WALLET_CONNECT_PROJECT_ID=363bf09c10fec2293b21ee199b2ce8d5
VITE_PRIVY_APP_ID=cm7u09v0u002zrkuf2yjjr58p

View File

@@ -18,11 +18,15 @@
"dependencies": {
"@heroicons/react": "^1.0.6",
"@microsoft/signalr": "^6.0.5",
"@tanstack/react-query": "^5.36.1",
"@privy-io/react-auth": "^2.6.1",
"@privy-io/wagmi": "^1.0.3",
"@tailwindcss/typography": "^0.5.0",
"@tanstack/react-query": "^5.67.1",
"@wagmi/chains": "^0.2.9",
"@wagmi/connectors": "^5.7.3",
"@wagmi/core": "^2.16.3",
"@walletconnect/universal-provider": "^2.8.6",
"autoprefixer": "^10.4.7",
"axios": "^0.27.2",
"classnames": "^2.3.1",
"connectkit": "^1.8.2",
@@ -32,6 +36,7 @@
"lightweight-charts": "git+https://github.com/ntf/lightweight-charts.git",
"moment": "^2.29.3",
"plotly.js": "^2.18.1",
"postcss": "^8.4.13",
"react": "^18.1.0",
"react-cookie": "^4.1.1",
"react-dom": "^18.1.0",
@@ -46,14 +51,11 @@
"react-table": "^7.8.0",
"react-toastify": "^9.0.1",
"reactflow": "^11.8.3",
"tailwindcss": "^3.0.23",
"viem": "2.x",
"wagmi": "^2.14.9",
"wagmi": "^2.14.12",
"web3": "^4.16.0",
"zustand": "^4.4.1",
"postcss": "^8.4.13",
"autoprefixer": "^10.4.7",
"@tailwindcss/typography": "^0.5.0",
"tailwindcss": "^3.0.23"
"zustand": "^4.4.1"
},
"devDependencies": {
"@types/react": "^18.0.9",

View File

@@ -0,0 +1,95 @@
import { usePrivy } from '@privy-io/react-auth'
import { useAccount, useBalance, useChainId, useSwitchChain } from 'wagmi'
import { arbitrum, mainnet } from 'viem/chains'
import { useCallback } from 'react'
/**
* Component to display wallet information and test the Privy integration
*/
export const WalletInfo = () => {
const { user, ready, authenticated, login, logout } = usePrivy()
const { address, isConnected } = useAccount()
const chainId = useChainId()
const { data: balance } = useBalance({
address,
})
const { switchChainAsync } = useSwitchChain()
const switchToEthereum = useCallback(async () => {
if (chainId !== mainnet.id) {
try {
await switchChainAsync({ chainId: mainnet.id })
} catch (error) {
console.error('Failed to switch to Ethereum:', error)
}
}
}, [chainId, switchChainAsync])
const switchToArbitrum = useCallback(async () => {
if (chainId !== arbitrum.id) {
try {
await switchChainAsync({ chainId: arbitrum.id })
} catch (error) {
console.error('Failed to switch to Arbitrum:', error)
}
}
}, [chainId, switchChainAsync])
if (!ready) {
return <div>Loading...</div>
}
if (!authenticated) {
return (
<div className="p-4 border rounded-lg shadow-sm">
<h2 className="text-xl font-bold mb-4">Wallet Connection</h2>
<p className="mb-4">Please connect your wallet to continue.</p>
<button
onClick={login}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
Connect Wallet
</button>
</div>
)
}
return (
<div className="p-4 border rounded-lg shadow-sm">
<h2 className="text-xl font-bold mb-4">Wallet Information</h2>
<div className="mb-4">
<p><strong>Connected:</strong> {isConnected ? 'Yes' : 'No'}</p>
<p><strong>Address:</strong> {address ? address : 'Not connected'}</p>
<p><strong>Chain:</strong> {chainId === mainnet.id ? 'Ethereum' : chainId === arbitrum.id ? 'Arbitrum' : chainId}</p>
<p><strong>Balance:</strong> {balance ? `${balance.formatted} ${balance.symbol}` : 'Unknown'}</p>
</div>
<div className="flex space-x-2 mb-4">
<button
onClick={switchToEthereum}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
disabled={chainId === mainnet.id}
>
Switch to Ethereum
</button>
<button
onClick={switchToArbitrum}
className="px-4 py-2 bg-purple-500 text-white rounded hover:bg-purple-600"
disabled={chainId === arbitrum.id}
>
Switch to Arbitrum
</button>
</div>
<button
onClick={logout}
className="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
>
Disconnect
</button>
</div>
)
}
export default WalletInfo

View File

@@ -1,7 +1,7 @@
import { StatusOfflineIcon } from '@heroicons/react/solid'
import type { SubmitHandler } from 'react-hook-form'
import { useForm } from 'react-hook-form'
import { useAccount, useDisconnect, useSignMessage } from 'wagmi'
import { usePrivy, useSignMessage } from '@privy-io/react-auth'
import useApiUrlStore from '../../../app/store/apiStore'
import { UserClient } from '../../../generated/ManagingApi'
@@ -13,38 +13,66 @@ import Toast from '../Toast/Toast'
const LogIn = () => {
const { apiUrl } = useApiUrlStore()
const { register, handleSubmit } = useForm<ILoginFormInput>()
const { disconnect } = useDisconnect()
const { address } = useAccount()
const { signMessageAsync } = useSignMessage({})
const { user, logout, ready, authenticated } = usePrivy()
const { signMessage } = useSignMessage()
const { setCookie } = useCookie()
const onSubmit: SubmitHandler<ILoginFormInput> = async (form) => {
const message = 'wagmi'
const signature = await signMessageAsync({ message })
const t = new Toast('Creating token')
if (signature && address) {
const userClient = new UserClient({}, apiUrl)
await userClient
.user_CreateToken({
address: address.toString(),
message: message,
name: form.name,
signature: signature,
})
.then((data) => {
setCookie('token', data, 1)
setTimeout(() => {
location.assign("/");
}, 1000);
})
.catch((err: any) => {
t.update('error', 'Error : Some thing bad happen')
})
}else{
t.update('error', 'Error : No signature')
if (!authenticated || !user || !user.wallet?.address) {
const t = new Toast('Error: Not authenticated')
t.update('error', 'Please connect your wallet first')
return
}
try {
const message = 'wagmi'
const t = new Toast('Signing message...')
// Use Privy's signMessage function - returns { signature: string }
const { signature } = await signMessage({ message })
t.update('info', 'Creating token...')
// Use the Privy embedded wallet address
const walletAddress = user.linkedAccounts[1]?.address
if (signature && walletAddress) {
const userClient = new UserClient({}, apiUrl)
await userClient
.user_CreateToken({
address: walletAddress,
message: message,
name: form.name,
signature: signature,
})
.then((data) => {
setCookie('token', data, 1)
t.update('success', 'Login successful!')
setTimeout(() => {
location.assign("/");
}, 1000);
})
.catch((err: any) => {
console.error('Login error:', err)
t.update('error', 'Error: Something went wrong')
})
} else {
t.update('error', 'Error: No signature or address')
}
} catch (error) {
console.error('Signing error:', error)
const t = new Toast('Error')
t.update('error', `Error signing message: ${error instanceof Error ? error.message : 'Unknown error'}`)
}
}
if (!ready) {
return <div>Loading...</div>
}
if (!authenticated) {
return <div>Please connect your wallet first</div>
}
return (
@@ -87,7 +115,8 @@ const LogIn = () => {
Sign and login
</button>
<button
onClick={() => disconnect}
onClick={() => logout()}
type="button"
className="btn bg-primary w-full text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800"
>
Disconnect wallet{' '}

View File

@@ -1,5 +1,5 @@
import { useIsFetching } from '@tanstack/react-query'
import { ConnectKitButton } from 'connectkit'
import { usePrivy } from '@privy-io/react-auth'
import type { ReactNode } from 'react'
import { useState } from 'react'
import { Link } from 'react-router-dom'
@@ -44,6 +44,38 @@ const GlobalLoader = () => {
return isFetching ? <Loader size="xs"></Loader> : null
}
// Custom Privy wallet button component
const PrivyWalletButton = () => {
const { login, logout, authenticated, user } = usePrivy()
if (!authenticated) {
return (
<button
onClick={login}
className="btn btn-primary btn-sm"
>
Connect Wallet
</button>
)
}
// Display wallet address or user info if authenticated
const displayAddress = user?.wallet?.address
? `${user.wallet.address.slice(0, 6)}...${user.wallet.address.slice(-4)}`
: 'Connected'
return (
<div className="dropdown dropdown-end">
<label tabIndex={0} className="btn btn-primary btn-sm">
{displayAddress}
</label>
<ul tabIndex={0} className="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52">
<li><a onClick={logout}>Disconnect</a></li>
</ul>
</div>
)
}
export function SecondaryNavbar() {
const { toggleApiUrl, isProd } = useApiUrlStore()
@@ -61,7 +93,7 @@ export function SecondaryNavbar() {
/>
</label>
</div>
<ConnectKitButton />
<PrivyWalletButton />
</div>
)
}

View File

@@ -0,0 +1,19 @@
import { arbitrum, mainnet } from 'viem/chains'
import { createConfig } from '@privy-io/wagmi'
import { http } from 'wagmi'
// Define the Privy App ID - this should be added to your .env file
const PRIVY_APP_ID = import.meta.env.VITE_PRIVY_APP_ID || 'your-privy-app-id'
// Create the Privy Wagmi config with Ethereum and Arbitrum chains
export const privyWagmiConfig = createConfig({
chains: [mainnet, arbitrum],
transports: {
// You can customize RPC providers here if needed
[mainnet.id]: http(`https://ethereum.publicnode.com`),
[arbitrum.id]: http(`https://arbitrum-one.publicnode.com`),
},
})
// Export the supported chains for use elsewhere in the app
export const supportedChains = [mainnet, arbitrum]

View File

@@ -0,0 +1,65 @@
import { usePrivy } from '@privy-io/react-auth'
import { useAccount, useConnect, useDisconnect } from 'wagmi'
import { useCallback, useEffect } from 'react'
/**
* Custom hook to integrate Privy wallet with wagmi
* This hook handles connecting and disconnecting the Privy wallet
* and provides a simplified interface for wallet interactions
*/
export const usePrivyWallet = () => {
const { user, ready, authenticated, login, logout } = usePrivy()
const { isConnected } = useAccount()
const { connectAsync, connectors } = useConnect()
const { disconnectAsync } = useDisconnect()
// Connect the wallet when the user is authenticated
useEffect(() => {
const connectWallet = async () => {
if (ready && authenticated && user && !isConnected) {
try {
// Get the embedded wallet if available
const embeddedWallet = user.wallet?.address ? user.wallet : null
if (embeddedWallet && connectors.length > 0) {
// Connect using the Privy connector
// The Privy connector is automatically added by the Privy WagmiProvider
const privyConnector = connectors.find(c => c.name === 'Privy') || connectors[0]
await connectAsync({
connector: privyConnector,
chainId: 1 // Default to Ethereum mainnet
})
}
} catch (error) {
console.error('Failed to connect wallet:', error)
}
}
}
connectWallet()
}, [ready, authenticated, user, isConnected, connectAsync, connectors])
// Disconnect the wallet when the user logs out
const handleLogout = useCallback(async () => {
if (isConnected) {
try {
await disconnectAsync()
} catch (error) {
console.error('Failed to disconnect wallet:', error)
}
}
logout()
}, [isConnected, disconnectAsync, logout])
return {
isAuthenticated: authenticated,
isConnected,
isReady: ready,
user,
login,
logout: handleLogout,
}
}
export default usePrivyWallet

View File

@@ -1,35 +1,47 @@
import './styles/globals.css'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ConnectKitProvider, getDefaultConfig } from 'connectkit'
import { PrivyProvider } from '@privy-io/react-auth'
import { WagmiProvider } from '@privy-io/wagmi'
import { createRoot } from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import { WagmiConfig, createConfig } from 'wagmi'
import App from './app'
import { privyWagmiConfig, supportedChains } from './config/privy'
import 'react-grid-layout/css/styles.css'
import 'react-resizable/css/styles.css'
import 'react-toastify/dist/ReactToastify.css'
import { ToastContainer } from 'react-toastify'
const config = createConfig(
getDefaultConfig({
alchemyId: import.meta.env.VITE_ALCHEMY_ID,
appName: 'Managing App',
walletConnectProjectId: import.meta.env.VITE_WALLET_CONNECT_PROJECT_ID,
})
)
const element = document.getElementById('root') as HTMLElement
const root = createRoot(element)
const queryClient = new QueryClient()
// Configure Privy login methods and appearance
const privyConfig = {
appearance: {
theme: 'light' as const,
accentColor: '#3B82F6' as `#${string}`, // Customize this to match your app's theme
logo: <img src="/src/assets/logo.svg" alt="logo" />,
},
loginMethods: ['wallet', 'email', 'google'] as Array<'wallet' | 'email' | 'google'>,
embeddedWallets: {
ethereum: {
createOnLogin: 'all-users' as const
},
noPromptOnSignature: false
},
}
root.render(
<QueryClientProvider client={queryClient}>
<WagmiConfig config={config}>
<ConnectKitProvider theme="auto">
<PrivyProvider
appId={import.meta.env.VITE_PRIVY_APP_ID}
config={privyConfig}
>
<WagmiProvider config={privyWagmiConfig}>
<BrowserRouter>
<App />
<ToastContainer
@@ -44,7 +56,7 @@ root.render(
pauseOnHover
/>
</BrowserRouter>
</ConnectKitProvider>
</WagmiConfig>
</WagmiProvider>
</PrivyProvider>
</QueryClientProvider>
)

View File

@@ -1,37 +1,50 @@
import { ConnectKitButton } from 'connectkit'
import { usePrivy } from '@privy-io/react-auth'
import { useAccount } from 'wagmi'
import LogIn from '../../components/mollecules/LogIn/LogIn'
import useCookie from '../../hooks/useCookie'
import { useEffect, useState } from 'react'
export const Auth = ({ children }: any) => {
const { getCookie, deleteCookie } = useCookie()
const { isConnected } = useAccount()
const { login, ready, authenticated, user } = usePrivy()
const token = getCookie('token')
console.log('token', token)
console.log('isConnected', isConnected)
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const timeout = setTimeout(() => {
setIsLoading(false);
}, 2000); // Adjust the timeout duration as needed
if (ready) {
const timeout = setTimeout(() => {
setIsLoading(false);
}, 1000);
return () => clearTimeout(timeout);
}
}, [ready]);
return () => clearTimeout(timeout);
}, []);
if (isLoading) {
if (!ready || isLoading) {
return <div>Loading...</div>;
}
if (!isConnected) {
if (!authenticated) {
deleteCookie('token')
return (
<div style={{ ...styles }}>
<ConnectKitButton />
<button
onClick={login}
style={{
padding: '10px 20px',
backgroundColor: '#3B82F6',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '16px'
}}
>
Login with Privy
</button>
</div>
)
} else if (!token) {
@@ -41,7 +54,6 @@ export const Auth = ({ children }: any) => {
}
}
const styles = {
alignItems: 'center',
display: 'flex',