mirror of
https://github.com/Permissionless-Software-Foundation/bch-dex-taker-v2.git
synced 2026-09-21 16:52:01 -07:00
feat(wallet): Ported Wallet View
This commit is contained in:
Generated
+85
-4
@@ -1,13 +1,17 @@
|
||||
{
|
||||
"name": "react-bootstrap-web3-spa",
|
||||
"version": "2.0.1",
|
||||
"name": "bch-wallet-web3-spa",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "react-bootstrap-web3-spa",
|
||||
"version": "2.0.1",
|
||||
"name": "bch-wallet-web3-spa",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "6.7.2",
|
||||
"@fortawesome/free-regular-svg-icons": "6.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "6.7.2",
|
||||
"@fortawesome/react-fontawesome": "0.2.2",
|
||||
"axios": "0.27.2",
|
||||
"bch-message-lib": "2.2.1",
|
||||
"bootstrap": "5.2.0",
|
||||
@@ -16,6 +20,7 @@
|
||||
"react-bootstrap": "2.10.7",
|
||||
"react-dom": "19.0.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"use-local-storage-state": "19.5.0",
|
||||
"use-query-params": "1.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -2057,6 +2062,64 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/fontawesome-common-types": {
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz",
|
||||
"integrity": "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/fontawesome-svg-core": {
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz",
|
||||
"integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.7.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/free-regular-svg-icons": {
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.7.2.tgz",
|
||||
"integrity": "sha512-7Z/ur0gvCMW8G93dXIQOkQqHo2M5HLhYrRVC0//fakJXxcF1VmMPsxnG6Ee8qEylA8b8Q3peQXWMNZ62lYF28g==",
|
||||
"license": "(CC-BY-4.0 AND MIT)",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.7.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/free-solid-svg-icons": {
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.2.tgz",
|
||||
"integrity": "sha512-GsBrnOzU8uj0LECDfD5zomZJIjrPhIlWU82AHwa2s40FKH+kcxQaBvBo3Z4TxyZHIyX8XTDxsyA33/Vx9eFuQA==",
|
||||
"license": "(CC-BY-4.0 AND MIT)",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.7.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/react-fontawesome": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.2.tgz",
|
||||
"integrity": "sha512-EnkrprPNqI6SXJl//m29hpaNzOp1bruISWaOiRtkMi/xSvHJlzc2j2JAYS7egxt/EbjSNV/k6Xy0AQI6vB2+1g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "~1 || ~6",
|
||||
"react": ">=16.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanwhocodes/config-array": {
|
||||
"version": "0.9.5",
|
||||
"license": "Apache-2.0",
|
||||
@@ -12792,6 +12855,8 @@
|
||||
"node_modules/node-fetch": {
|
||||
"name": "@achingbrain/node-fetch",
|
||||
"version": "2.6.7",
|
||||
"resolved": "https://registry.npmjs.org/@achingbrain/node-fetch/-/node-fetch-2.6.7.tgz",
|
||||
"integrity": "sha512-iTASGs+HTFK5E4ZqcMsHmeJ4zodyq8L38lZV33jwqcBJYoUt3HjN4+ot+O9/0b+ke8ddE7UgOtVuZN/OkV19/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -17340,6 +17405,22 @@
|
||||
"requires-port": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/use-local-storage-state": {
|
||||
"version": "19.5.0",
|
||||
"resolved": "https://registry.npmjs.org/use-local-storage-state/-/use-local-storage-state-19.5.0.tgz",
|
||||
"integrity": "sha512-sUJAyFvsmqMpBhdwaRr7GTKkkoxb6PWeNVvpBDrLuwQF1PpbJRKIbOYeLLeqJI7B3wdfFlLLCBbmOdopiSTBOw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/astoilkov"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/use-query-params": {
|
||||
"version": "1.2.3",
|
||||
"license": "ISC",
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
"name": "bch-wallet-web3-spa",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "6.7.2",
|
||||
"@fortawesome/free-regular-svg-icons": "6.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "6.7.2",
|
||||
"@fortawesome/react-fontawesome": "0.2.2",
|
||||
"axios": "0.27.2",
|
||||
"bch-message-lib": "2.2.1",
|
||||
"bootstrap": "5.2.0",
|
||||
@@ -10,6 +14,7 @@
|
||||
"react-bootstrap": "2.10.7",
|
||||
"react-dom": "19.0.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"use-local-storage-state": "19.5.0",
|
||||
"use-query-params": "1.2.3"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
+15
-1
@@ -30,6 +30,7 @@ function App (props) {
|
||||
return prevBody
|
||||
})
|
||||
}, [])
|
||||
|
||||
/** Load all required data before component start. */
|
||||
useEffect(() => {
|
||||
async function asyncEffect () {
|
||||
@@ -55,8 +56,21 @@ function App (props) {
|
||||
addToModal('Initializing wallet', appData)
|
||||
console.log(`Initializing wallet with back end server ${appData.serverUrl}`)
|
||||
|
||||
const walletTemp = await asyncLoad.initWallet(appData.serverUrl)
|
||||
const walletTemp = await asyncLoad.initWallet(appData.serverUrl, appData.lsState.mnemonic, appData)
|
||||
appData.setWallet(walletTemp)
|
||||
// appData.updateBchWalletState({ walletObj: walletTemp.walletInfo, appData })
|
||||
|
||||
// Get the BCH balance of the wallet.
|
||||
addToModal('Getting BCH balance', appData)
|
||||
await asyncLoad.getWalletBchBalance(walletTemp, appData.updateBchWalletState, appData)
|
||||
|
||||
// Get the SLP tokens held by the wallet.
|
||||
addToModal('Getting SLP tokens', appData)
|
||||
await asyncLoad.getSlpTokenBalances(walletTemp, appData.updateBchWalletState, appData)
|
||||
|
||||
// Get the BCH spot price
|
||||
addToModal('Getting BCH spot price in USD', appData)
|
||||
await asyncLoad.getUSDExchangeRate(walletTemp, appData.updateBchWalletState, appData)
|
||||
|
||||
// Update state
|
||||
appData.setShowStartModal(false)
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
This Card component is used to clear the Local Storage and reset the wallet.
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
import React from 'react'
|
||||
import { Container, Row, Col, Card, Button } from 'react-bootstrap'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faTriangleExclamation } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
const WalletClear = (props) => {
|
||||
const { removeLocalStorageItem } = props.appData
|
||||
|
||||
// Delete wallet data from Local Storage and reload the app.
|
||||
const handleClearLocalStorage = () => {
|
||||
console.log('Deleting wallet and reloading page.')
|
||||
// Delete the mnemonic from Local Storage
|
||||
removeLocalStorageItem('mnemonic')
|
||||
// Reload the app.
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container>
|
||||
<Row>
|
||||
<Col>
|
||||
<Card>
|
||||
<Card.Body>
|
||||
<Card.Title style={{ textAlign: 'center' }}>
|
||||
<h2>
|
||||
<FontAwesomeIcon icon={faTriangleExclamation} />{' '}
|
||||
<span>Clear Local Storage</span>
|
||||
</h2>
|
||||
</Card.Title>
|
||||
|
||||
<Card.Text style={{ textAlign: 'center' }}>
|
||||
Clicking the button below will clear the Local Storage, which
|
||||
will reload the app with a newly created wallet.
|
||||
<br />
|
||||
<b>
|
||||
Be sure to write down your 12-word mnemonic to back
|
||||
up your wallet before clicking the button!
|
||||
</b>.
|
||||
<br /><br />
|
||||
<Button variant='danger' onClick={handleClearLocalStorage}>
|
||||
Delete Wallet
|
||||
</Button>
|
||||
</Card.Text>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default WalletClear
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
This component is visually represented with a copy icon. A wallet property
|
||||
is passed as a prop. When clicked, the wallet property is copied to the
|
||||
system clipboard.
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faCopy } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
const CopyOnClick = (props) => {
|
||||
// State
|
||||
const [iconVis, setIconVis] = useState(true)
|
||||
// Props
|
||||
const { appData, walletProp, value } = props
|
||||
// App Util
|
||||
const { appUtil } = appData
|
||||
|
||||
// Function to copy the value to the clipboard.
|
||||
const handleCopyToClipboard = useCallback(async (event) => {
|
||||
appUtil.copyToClipboard(value)
|
||||
|
||||
// hide icon in order to show the copied message
|
||||
setIconVis(false)
|
||||
|
||||
// restart icon visibility after 1 second
|
||||
setTimeout(function () {
|
||||
setIconVis(true)
|
||||
}, 1000)
|
||||
}, [value, appUtil])
|
||||
|
||||
return (
|
||||
<>
|
||||
{iconVis && (
|
||||
<FontAwesomeIcon
|
||||
icon={faCopy} size='lg'
|
||||
id={`${walletProp}-icon`}
|
||||
onClick={(e) => handleCopyToClipboard(e)}
|
||||
style={{ cursor: 'pointer' }}
|
||||
/>
|
||||
)}
|
||||
{!iconVis && (
|
||||
<span
|
||||
id={`${walletProp}-word`}
|
||||
style={{ color: 'green' }}
|
||||
>
|
||||
Copied!
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default CopyOnClick
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
This component allows the user to import a new wallet using a 12-word mnemonic.
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
import React, { useCallback } from 'react'
|
||||
import { Container, Row, Col, Card, Button, Form } from 'react-bootstrap'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faFileExport, faPaste } from '@fortawesome/free-solid-svg-icons'
|
||||
// import { Clipboard } from '@capacitor/clipboard'
|
||||
|
||||
const WalletImport = (props) => {
|
||||
const [newMnemonic, setNewMnemonic] = React.useState('')
|
||||
const { appData } = props
|
||||
|
||||
// Load mnemonic from clipboard
|
||||
const pasteFromClipboard = async () => {
|
||||
try {
|
||||
const mnemonic = await appData.appUtil.readFromClipboard()
|
||||
setNewMnemonic(mnemonic)
|
||||
} catch (err) {
|
||||
console.warn('Error pasting from clipboard: ', err)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle input change for mnemonic
|
||||
const handleImportMnemonic = async (event) => {
|
||||
const inputStr = event.target.value
|
||||
const formattedInput = inputStr.toLowerCase()
|
||||
setNewMnemonic(formattedInput)
|
||||
}
|
||||
|
||||
// Ensure the mnemonic is valid. If it is, then replace the current mnemonic
|
||||
// in LocalStorage and reload the page.
|
||||
const handleImportWallet = useCallback(async (event) => {
|
||||
try {
|
||||
const mnemonic = newMnemonic
|
||||
const wallet = appData.wallet
|
||||
const bchjs = wallet.bchjs
|
||||
|
||||
// Verify the mnemonic is valid.
|
||||
const isValid = bchjs.Mnemonic.validate(mnemonic, bchjs.Mnemonic.wordLists().english)
|
||||
if (isValid.includes('is not in wordlist')) {
|
||||
console.log('Mnemonic is NOT valid')
|
||||
} else {
|
||||
console.log('Mnemonic is valid')
|
||||
}
|
||||
|
||||
// Replace the old mnemonic in LocalStorage with the new one.
|
||||
appData.updateLocalStorage({ mnemonic })
|
||||
// Reload the app.
|
||||
window.location.reload()
|
||||
} catch (error) {
|
||||
console.warn('Error importing wallet: ', error)
|
||||
}
|
||||
}, [newMnemonic, appData])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container>
|
||||
<Row>
|
||||
<Col>
|
||||
<Card>
|
||||
<Card.Body>
|
||||
<Card.Title style={{ textAlign: 'center' }}>
|
||||
<h2>
|
||||
<FontAwesomeIcon icon={faFileExport} />{' '}
|
||||
<span>Import Wallet</span>
|
||||
</h2>
|
||||
</Card.Title>
|
||||
|
||||
<Card.Text style={{ textAlign: 'center' }}>
|
||||
Enter a 12 word mnemonic below to import your wallet into
|
||||
this app. The app will reload and use the new mnemonic.
|
||||
</Card.Text>
|
||||
|
||||
<Container>
|
||||
<Row>
|
||||
<Col xs={12} className='text-break' style={{ textAlign: 'center' }}>
|
||||
<Form>
|
||||
<Form.Group className='mb-3' controlId='formImportWallet' style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Form.Control
|
||||
style={{ margin: '1rem' }}
|
||||
type='text'
|
||||
value={newMnemonic}
|
||||
onChange={handleImportMnemonic}
|
||||
/>
|
||||
<FontAwesomeIcon
|
||||
icon={faPaste}
|
||||
size='lg'
|
||||
onClick={(e) => pasteFromClipboard(e)}
|
||||
style={{ cursor: 'pointer' }}
|
||||
/>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<Col style={{ textAlign: 'center' }}>
|
||||
<Button variant='primary' onClick={handleImportWallet}>
|
||||
Import
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<br />
|
||||
</Container>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default WalletImport
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
This component controlls the Wallet View.
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
import React from 'react'
|
||||
import { Container, Row, Col } from 'react-bootstrap'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faCircleQuestion } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
// Local Libraries
|
||||
import WebWalletWarning from './warning'
|
||||
import WalletSummary from './wallet-summary'
|
||||
import WalletClear from './clear-wallet'
|
||||
import WalletImport from './import-wallet'
|
||||
import OptimizeWallet from './optimize-wallet'
|
||||
|
||||
function BchWallet (props) {
|
||||
// Dependency injection through props
|
||||
const appData = props.appData
|
||||
console.log('appData: ', appData)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container>
|
||||
<Row>
|
||||
<Col style={{ textAlign: 'right' }}>
|
||||
<a href='https://youtu.be/0R00cppN0fA' target='_blank' rel='noreferrer'>
|
||||
<FontAwesomeIcon icon={faCircleQuestion} size='lg' />
|
||||
</a>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
<WebWalletWarning />
|
||||
<br />
|
||||
<WalletSummary appData={appData} />
|
||||
<br />
|
||||
<WalletClear appData={appData} />
|
||||
<br />
|
||||
<WalletImport appData={appData} />
|
||||
<br />
|
||||
<OptimizeWallet appData={appData} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default BchWallet
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
This component allows the user to optimize their wallet by consolidating
|
||||
UTXOs. This speeds up all the network calls and results in an improved UX.
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
import React, { useState } from 'react'
|
||||
import { Container, Row, Col, Card, Button } from 'react-bootstrap'
|
||||
|
||||
// Local libraries
|
||||
import WaitingModal from '../../waiting-modal'
|
||||
|
||||
function OptimizeWallet (props) {
|
||||
// State
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const [modalBody, setModalBody] = useState([])
|
||||
const [hideSpinner, setHideSpinner] = useState(false)
|
||||
const [denyClose, setDenyClose] = useState(false)
|
||||
|
||||
// Get props values
|
||||
const { wallet } = props.appData
|
||||
|
||||
// Optimize wallet
|
||||
const handleOptimize = async () => {
|
||||
console.log('Optimize Wallet button clicked.')
|
||||
// Show waiting modal
|
||||
setShowModal(true)
|
||||
setModalBody(['Optimizing wallet...'])
|
||||
setDenyClose(true)
|
||||
|
||||
// Optimize wallet
|
||||
await wallet.optimize()
|
||||
|
||||
// Show success modal
|
||||
setShowModal(true)
|
||||
setModalBody(['Your wallet has been optimized!'])
|
||||
setDenyClose(false)
|
||||
setHideSpinner(true)
|
||||
|
||||
try {
|
||||
// Get all UTXOs in the wallet
|
||||
const utxos = wallet.utxos.utxoStore
|
||||
console.log('utxos: ', utxos)
|
||||
|
||||
// Add up all the UTXOs
|
||||
const bchUtxoCnt = utxos.bchUtxos.length
|
||||
let fungibleUtxoCnt = utxos.slpUtxos.type1.tokens.length
|
||||
if (!fungibleUtxoCnt) fungibleUtxoCnt = 0
|
||||
let nftUtxoCnt = utxos.slpUtxos.nft.length
|
||||
if (!nftUtxoCnt) nftUtxoCnt = 0
|
||||
const totalUtxos = bchUtxoCnt + fungibleUtxoCnt + nftUtxoCnt
|
||||
console.log(`bchUtxoCnt: ${bchUtxoCnt}, fungibleUtxoCnt: ${fungibleUtxoCnt}, nftUtxoCnt: ${nftUtxoCnt}`)
|
||||
console.log(`total UTXO count: ${totalUtxos}`)
|
||||
|
||||
if (totalUtxos > 10) {
|
||||
const newModalBody = [
|
||||
'Your wallet has been optimized!',
|
||||
'Your wallet still has more than 10 UTXOs. Increased numbers of UTXOs slow down performance. If you have several tokens in your wallet, it is recommended that you store them in a paper wallet. Here is a video explaining how to do that:'
|
||||
]
|
||||
|
||||
newModalBody.push(<a href='https://youtu.be/mRniqpgWdjg' target='_blank' rel='noreferrer'>Video: How to Store SLP Tokens on a Paper Wallet</a>)
|
||||
newModalBody.push(<a href='https://paperwallet.fullstack.cash/' target='_blank' rel='noreferrer'>Generate a Paper Wallet</a>)
|
||||
|
||||
setModalBody(newModalBody)
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('Error while trying to count total number of UTXOs: ', err)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container>
|
||||
<Row>
|
||||
<Col>
|
||||
<Card>
|
||||
<Card.Body>
|
||||
<Card.Title style={{ textAlign: 'center' }}>
|
||||
<h2>Optimize Wallet</h2>
|
||||
</Card.Title>
|
||||
<Card.Text style={{ textAlign: 'center' }}>
|
||||
|
||||
Clicking the button below will optimize your wallet and make it
|
||||
function faster.
|
||||
<br /><br />
|
||||
<i>How it works</i>: By consolidating
|
||||
as many UTXOs in your wallet as possible, it reduces the total
|
||||
number of UTXOs in your wallet. Fewer UTXOs in your wallet make
|
||||
all network calls faster, and results in an improved user experience.
|
||||
<br /><br />
|
||||
<Button variant='primary' onClick={handleOptimize}>
|
||||
Optimize Wallet
|
||||
</Button>
|
||||
</Card.Text>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
|
||||
{showModal && (
|
||||
<WaitingModal
|
||||
heading='Optimizing Wallet'
|
||||
body={modalBody}
|
||||
hideSpinner={hideSpinner}
|
||||
denyClose={denyClose}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default OptimizeWallet
|
||||
@@ -0,0 +1,4 @@
|
||||
.blurred {
|
||||
filter: blur(6px);
|
||||
-webkit-filter: blur(6px);
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
This component displays a summary of the wallet.
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
import React, { useState } from 'react'
|
||||
import { Container, Row, Col, Card } from 'react-bootstrap'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faWallet, faEye, faEyeSlash } from '@fortawesome/free-solid-svg-icons'
|
||||
// import { Clipboard } from '@capacitor/clipboard'
|
||||
|
||||
// Local libraries
|
||||
import './wallet-summary.css'
|
||||
import CopyOnClick from './copy-on-click'
|
||||
|
||||
function WalletSummary (props) {
|
||||
// Props
|
||||
const appData = props.appData
|
||||
|
||||
const bchWalletState = appData.bchWalletState
|
||||
console.log('wallet summary state: ', bchWalletState)
|
||||
|
||||
// State
|
||||
const [blurredMnemonic, setBlurredMnemonic] = useState(true)
|
||||
const [blurredPrivateKey, setBlurredPrivateKey] = useState(true)
|
||||
|
||||
// Encapsulate component state into an object that can be passed to child functions
|
||||
const walletSummaryData = {
|
||||
blurredMnemonic,
|
||||
setBlurredMnemonic,
|
||||
blurredPrivateKey,
|
||||
setBlurredPrivateKey
|
||||
}
|
||||
|
||||
// Eye icon state
|
||||
const eyeIcon = {
|
||||
mnemonic: blurredMnemonic ? faEyeSlash : faEye,
|
||||
privateKey: blurredPrivateKey ? faEyeSlash : faEye
|
||||
}
|
||||
|
||||
// Toggle the state of blurring for the mnemonic
|
||||
const toggleMnemonicBlur = (inObj = {}) => {
|
||||
try {
|
||||
const { walletSummaryData } = inObj
|
||||
|
||||
// toggle the state of blurring
|
||||
const blurredState = walletSummaryData.blurredMnemonic
|
||||
walletSummaryData.setBlurredMnemonic(!blurredState)
|
||||
} catch (error) {
|
||||
console.error('Error toggling mnemonic blur: ', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle the state of blurring for the private key
|
||||
const togglePrivateKeyBlur = (inObj = {}) => {
|
||||
try {
|
||||
const { walletSummaryData } = inObj
|
||||
|
||||
// toggle the state of blurring
|
||||
const blurredState = walletSummaryData.blurredPrivateKey
|
||||
walletSummaryData.setBlurredPrivateKey(!blurredState)
|
||||
} catch (error) {
|
||||
console.error('Error toggling private key blur: ', error)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container>
|
||||
<Row>
|
||||
<Col>
|
||||
<Card>
|
||||
<Card.Body>
|
||||
<Card.Title style={{ textAlign: 'center' }}>
|
||||
<h2>
|
||||
<FontAwesomeIcon icon={faWallet} />{' '}
|
||||
<span>My Wallet</span>
|
||||
</h2>
|
||||
</Card.Title>
|
||||
<Container>
|
||||
<Row style={{ padding: '25px' }}>
|
||||
<Col xs={12} sm={10} lg={8} style={{ padding: '10px' }}>
|
||||
<b>Mnemonic:</b> <span className={blurredMnemonic ? 'blurred' : null}>{bchWalletState.mnemonic}</span>
|
||||
</Col>
|
||||
<Col xs={6} sm={1} lg={2} style={{ textAlign: 'center' }}>
|
||||
<FontAwesomeIcon
|
||||
style={{ cursor: 'pointer' }}
|
||||
icon={eyeIcon.mnemonic}
|
||||
size='lg'
|
||||
onClick={() => toggleMnemonicBlur({ walletSummaryData })}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={6} sm={1} lg={2} style={{ textAlign: 'center' }}>
|
||||
<CopyOnClick walletProp='mnemonic' appData={appData} value={bchWalletState.mnemonic} />
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row style={{ padding: '25px', backgroundColor: '#eee' }}>
|
||||
<Col xs={12} sm={10} lg={8} style={{ padding: '10px' }}>
|
||||
<b>Private Key:</b> <span className={blurredPrivateKey ? 'blurred' : null}>{bchWalletState.privateKey}</span>
|
||||
</Col>
|
||||
<Col xs={6} sm={1} lg={2} style={{ textAlign: 'center' }}>
|
||||
<FontAwesomeIcon
|
||||
style={{ cursor: 'pointer' }}
|
||||
icon={eyeIcon.privateKey}
|
||||
size='lg'
|
||||
onClick={() => togglePrivateKeyBlur({ walletSummaryData })}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={6} sm={1} lg={2} style={{ textAlign: 'center' }}>
|
||||
<CopyOnClick walletProp='privateKey' appData={appData} value={bchWalletState.privateKey} />
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row style={{ padding: '25px' }}>
|
||||
<Col xs={12} sm={10} lg={8} style={{ padding: '10px' }}>
|
||||
<b>Cash Address:</b> {bchWalletState.cashAddress}
|
||||
</Col>
|
||||
<Col xs={6} sm={1} lg={2} style={{ textAlign: 'center' }} />
|
||||
<Col xs={6} sm={1} lg={2} style={{ textAlign: 'center' }}>
|
||||
<CopyOnClick walletProp='cashAddress' appData={appData} value={bchWalletState.cashAddress} />
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row style={{ padding: '25px', backgroundColor: '#eee' }}>
|
||||
<Col xs={12} sm={10} lg={8} style={{ padding: '10px' }}>
|
||||
<b>SLP Address:</b> {bchWalletState.slpAddress}
|
||||
</Col>
|
||||
<Col xs={6} sm={1} lg={2} style={{ textAlign: 'center' }} />
|
||||
<Col xs={6} sm={1} lg={2} style={{ textAlign: 'center' }}>
|
||||
<CopyOnClick walletProp='slpAddress' appData={appData} value={bchWalletState.slpAddress} />
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row style={{ padding: '25px' }}>
|
||||
<Col xs={12} sm={10} lg={8} style={{ padding: '10px' }}>
|
||||
<b>Legacy Address:</b> {bchWalletState.legacyAddress}
|
||||
</Col>
|
||||
<Col xs={6} sm={1} lg={2} style={{ textAlign: 'center' }} />
|
||||
<Col xs={6} sm={1} lg={2} style={{ textAlign: 'center' }}>
|
||||
<CopyOnClick walletProp='legacyAddress' appData={appData} value={bchWalletState.legacyAddress} />
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row style={{ padding: '25px', backgroundColor: '#eee' }}>
|
||||
<Col xs={10} sm={10} lg={8} style={{ padding: '10px' }}>
|
||||
<b>HD Path:</b> {bchWalletState.hdPath}
|
||||
</Col>
|
||||
<Col xs={6} sm={1} lg={2} style={{ textAlign: 'center' }} />
|
||||
<Col xs={6} sm={1} lg={2} style={{ textAlign: 'center' }}>
|
||||
<CopyOnClick walletProp='hdPath' appData={appData} value={bchWalletState.hdPath} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default WalletSummary
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
This component is a visual warning against storing large sums of money in
|
||||
a web wallet.
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
import React from 'react'
|
||||
import { Container, Row, Col, Card } from 'react-bootstrap'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faTriangleExclamation } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
const WebWalletWarning = () => {
|
||||
return (
|
||||
<>
|
||||
<Container>
|
||||
<Row>
|
||||
<Col>
|
||||
<Card>
|
||||
<Card.Body>
|
||||
<Card.Title style={{ textAlign: 'center' }}>
|
||||
<h2>
|
||||
<FontAwesomeIcon icon={faTriangleExclamation} />{' '}
|
||||
<span>Web Wallets are Insecure</span>
|
||||
</h2>
|
||||
</Card.Title>
|
||||
|
||||
<Card.Text style={{ textAlign: 'center' }}>
|
||||
This is an open source, non-custodial web wallet
|
||||
supporting Bitcoin Cash (BCH) and SLP tokens.
|
||||
It is optimized for convenience and not security.
|
||||
<br />
|
||||
<b>Do not store large amounts of money on a web wallet.
|
||||
</b>
|
||||
<br /><br />
|
||||
Note: Scammers frequently copy this open source code to build
|
||||
apps for stealing people's money. Be sure you trust the source
|
||||
serving you this app.
|
||||
</Card.Text>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default WebWalletWarning
|
||||
@@ -11,6 +11,7 @@ import React from 'react'
|
||||
|
||||
// Local libraries
|
||||
import GetBalance from './balance'
|
||||
import Wallet from './bch-wallet'
|
||||
import Placeholder2 from './placeholder2'
|
||||
import Placeholder3 from './placeholder3'
|
||||
import ServerSelectView from './servers/select-server-view'
|
||||
@@ -28,11 +29,13 @@ function AppBody (props) {
|
||||
case 0:
|
||||
return (<GetBalance wallet={appData.wallet} />)
|
||||
case 1:
|
||||
return (<Placeholder2 />)
|
||||
return (<Wallet appData={appData} />)
|
||||
case 2:
|
||||
return (<Placeholder2 />)
|
||||
case 3:
|
||||
return (<Placeholder3 />)
|
||||
|
||||
// Special Views
|
||||
// Special Views
|
||||
case 100:
|
||||
return (<ServerSelectView appData={appData} />)
|
||||
default:
|
||||
|
||||
@@ -35,6 +35,13 @@ function NavMenu (props) {
|
||||
>
|
||||
Check Balance
|
||||
</Nav.Link>
|
||||
<Nav.Link
|
||||
className={selectedMenu === 1 ? 'nav-link-active' : 'nav-link-inactive'}
|
||||
href='#'
|
||||
onClick={(e) => handleClickEvent(1)}
|
||||
>
|
||||
Wallet
|
||||
</Nav.Link>
|
||||
<Nav.Link
|
||||
className={selectedMenu === 1 ? 'nav-link-active' : 'nav-link-inactive'}
|
||||
href='#'
|
||||
|
||||
+70
-1
@@ -1,5 +1,7 @@
|
||||
import { useState } from 'react'
|
||||
import { useQueryParam, StringParam } from 'use-query-params'
|
||||
import useLocalStorageState from 'use-local-storage-state'
|
||||
import AppUtil from '../util'
|
||||
|
||||
function useAppState () {
|
||||
// Get the CashStack URL from query parameter or use default
|
||||
@@ -22,6 +24,65 @@ function useAppState () {
|
||||
const [hideSpinner, setHideSpinner] = useState(false)
|
||||
const [denyClose, setDenyClose] = useState(false)
|
||||
|
||||
// The wallet state makes this a true progressive web app (PWA). As
|
||||
// balances, UTXOs, and tokens are retrieved, this state is updated.
|
||||
// properties are enumerated here for the purpose of documentation.
|
||||
|
||||
// Local storage
|
||||
const [lsState, setLSState, { removeItem }] = useLocalStorageState('bchWalletState', {
|
||||
ssr: true,
|
||||
defaultValue: {}
|
||||
})
|
||||
console.log('lsState: ', lsState)
|
||||
const removeLocalStorageItem = removeItem
|
||||
const updateLocalStorage = (lsObj) => {
|
||||
// console.log(`updateLocalStorage() input: ${JSON.stringify(lsObj, null, 2)}`)
|
||||
|
||||
// Progressively overwrite the LocalStorage state.
|
||||
const newObj = Object.assign({}, lsState, lsObj)
|
||||
// console.log(`updateLocalStorage() output: ${JSON.stringify(newObj, null, 2)}`)
|
||||
|
||||
setLSState(newObj)
|
||||
}
|
||||
|
||||
const [bchWalletState, setBchWalletState] = useState({
|
||||
mnemonic: undefined,
|
||||
address: undefined,
|
||||
cashAddress: undefined,
|
||||
slpAddress: undefined,
|
||||
privateKey: undefined,
|
||||
publicKey: undefined,
|
||||
legacyAddress: undefined,
|
||||
hdPath: undefined,
|
||||
bchBalance: 0,
|
||||
slpTokens: [],
|
||||
bchUsdPrice: 150
|
||||
})
|
||||
|
||||
// This function is passed to child components in order to update the wallet
|
||||
// state. This function is important to make this wallet a PWA.
|
||||
function updateBchWalletState (inObj = {}) {
|
||||
try {
|
||||
const { walletObj, appData } = inObj
|
||||
|
||||
// Debuging
|
||||
// console.log('updateBchWalletState() walletObj: ', walletObj)
|
||||
// console.log('updateBchWalletState() appData: ', appData)
|
||||
|
||||
appData.setBchWalletState(oldState => {
|
||||
const newBchWalletState = Object.assign({}, oldState, walletObj)
|
||||
// console.log('newBchWalletState: ', newBchWalletState)
|
||||
|
||||
return newBchWalletState
|
||||
})
|
||||
|
||||
// console.log(`New wallet state: ${JSON.stringify(bchWalletState, null, 2)}`)
|
||||
} catch (err) {
|
||||
console.error('Error in App.js updateBchWalletState()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
serverUrl,
|
||||
setServerUrl,
|
||||
@@ -44,7 +105,15 @@ function useAppState () {
|
||||
hideSpinner,
|
||||
setHideSpinner,
|
||||
denyClose,
|
||||
setDenyClose
|
||||
setDenyClose,
|
||||
bchWalletState,
|
||||
setBchWalletState,
|
||||
lsState,
|
||||
setLSState,
|
||||
removeLocalStorageItem,
|
||||
updateLocalStorage,
|
||||
updateBchWalletState,
|
||||
appUtil: new AppUtil()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+136
-38
@@ -16,73 +16,171 @@ class AsyncLoad {
|
||||
// Load the minimal-slp-wallet which comes in as a <script> file and is
|
||||
// attached to the global 'window' object.
|
||||
async loadWalletLib () {
|
||||
do {
|
||||
// console.log('window.SlpWallet: ', window.SlpWallet)
|
||||
if (typeof window !== 'undefined' && window.SlpWallet) {
|
||||
this.BchWallet = window.SlpWallet
|
||||
try {
|
||||
do {
|
||||
if (typeof window !== 'undefined' && window.SlpWallet) {
|
||||
this.BchWallet = window.SlpWallet
|
||||
|
||||
return this.BchWallet
|
||||
} else {
|
||||
console.log('Waiting for wallet library to load...')
|
||||
}
|
||||
return this.BchWallet
|
||||
} else {
|
||||
console.log('Waiting for wallet library to load...')
|
||||
}
|
||||
|
||||
await sleep(1000)
|
||||
} while (!this.BchWallet)
|
||||
await sleep(1000)
|
||||
} while (!this.BchWallet)
|
||||
} catch (error) {
|
||||
console.error('Error loading wallet library: ', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the BCH wallet
|
||||
async initWallet (restURL) {
|
||||
const options = {
|
||||
interface: 'consumer-api',
|
||||
restURL,
|
||||
noUpdate: true
|
||||
async initWallet (restURL, mnemonic, appData) {
|
||||
try {
|
||||
const options = {
|
||||
interface: 'consumer-api',
|
||||
restURL,
|
||||
noUpdate: true
|
||||
}
|
||||
|
||||
let wallet
|
||||
if (mnemonic) {
|
||||
// Load the wallet from the mnemonic, if it's available from local storage.
|
||||
wallet = new this.BchWallet(mnemonic, options)
|
||||
} else {
|
||||
// Generate a new mnemonic and wallet.
|
||||
wallet = new this.BchWallet(null, options)
|
||||
}
|
||||
|
||||
// Wait for wallet to initialize.
|
||||
await wallet.walletInfoPromise
|
||||
await wallet.initialize()
|
||||
console.log('starting to update wallet state.')
|
||||
// Update the state of the wallet.
|
||||
appData.updateBchWalletState({ walletObj: wallet.walletInfo, appData })
|
||||
console.log('finished updating wallet state.')
|
||||
// Save the mnemonic to local storage.
|
||||
if (!mnemonic) {
|
||||
const newMnemonic = wallet.walletInfo.mnemonic
|
||||
appData.updateLocalStorage({ mnemonic: newMnemonic })
|
||||
}
|
||||
|
||||
this.wallet = wallet
|
||||
|
||||
return wallet
|
||||
} catch (error) {
|
||||
console.error('Error initializing wallet: ', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const wallet = new this.BchWallet(null, options)
|
||||
// Get the BCH balance of the wallet.
|
||||
async getWalletBchBalance (wallet, updateBchWalletState, appData) {
|
||||
try {
|
||||
// Get the BCH balance of the wallet.
|
||||
const bchBalance = await wallet.getBalance({ bchAddress: wallet.walletInfo.cashAddress })
|
||||
|
||||
await wallet.walletInfoPromise
|
||||
// console.log(`mnemonic: ${wallet.walletInfo.mnemonic}`)
|
||||
// Update the state of the wallet with the balances
|
||||
updateBchWalletState({ walletObj: { bchBalance }, appData })
|
||||
|
||||
this.wallet = wallet
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Error getting wallet BCH balance: ', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
return wallet
|
||||
// Get the spot exchange rate for BCH in USD.
|
||||
async getUSDExchangeRate (wallet, updateBchWalletState, appData) {
|
||||
try {
|
||||
const bchUsdPrice = await wallet.getUsd()
|
||||
|
||||
// Update the state of the wallet
|
||||
updateBchWalletState({ walletObj: { bchUsdPrice }, appData })
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Error getting USD exchange rate: ', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Get a list of SLP tokens held by the wallet.
|
||||
async getSlpTokenBalances (wallet, updateBchWalletState, appData) {
|
||||
try {
|
||||
// Get token information from the wallet. This will also initialize the UTXO store.
|
||||
const slpTokens = await wallet.listTokens(wallet.walletInfo.cashAddress)
|
||||
// console.log('slpTokens: ', slpTokens)
|
||||
|
||||
// Add an icon property to each token.
|
||||
slpTokens.map(x => {
|
||||
x.icon = ''// (<Jdenticon size='100' value={x.tokenId} />)
|
||||
x.iconNeedsDownload = true
|
||||
return true
|
||||
})
|
||||
|
||||
console.log('slpTokens: ', slpTokens)
|
||||
|
||||
// Update the state of the wallet with the balances
|
||||
updateBchWalletState({ walletObj: { slpTokens }, appData })
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Error getting SLP tokens', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Get token data for a given Token ID
|
||||
async getTokenData (tokenId) {
|
||||
const tokenData = await this.wallet.getTokenData(tokenId)
|
||||
try {
|
||||
const tokenData = await this.wallet.getTokenData(tokenId)
|
||||
|
||||
// Convert the IPFS CIDs into actual data.
|
||||
tokenData.immutableData = await this.getIpfsData(tokenData.immutableData)
|
||||
tokenData.mutableData = await this.getIpfsData(tokenData.mutableData)
|
||||
// Convert the IPFS CIDs into actual data.
|
||||
tokenData.immutableData = await this.getIpfsData(tokenData.immutableData)
|
||||
tokenData.mutableData = await this.getIpfsData(tokenData.mutableData)
|
||||
|
||||
return tokenData
|
||||
return tokenData
|
||||
} catch (error) {
|
||||
console.error('Error getting Token data', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Get data about a Group token
|
||||
async getGroupData (tokenId) {
|
||||
const tokenData = await this.getTokenData(tokenId)
|
||||
try {
|
||||
const tokenData = await this.getTokenData(tokenId)
|
||||
|
||||
const groupData = {
|
||||
immutableData: tokenData.immutableData,
|
||||
mutableData: tokenData.mutableData,
|
||||
nfts: tokenData.genesisData.nfts,
|
||||
tokenId: tokenData.genesisData.tokenId
|
||||
const groupData = {
|
||||
immutableData: tokenData.immutableData,
|
||||
mutableData: tokenData.mutableData,
|
||||
nfts: tokenData.genesisData.nfts,
|
||||
tokenId: tokenData.genesisData.tokenId
|
||||
}
|
||||
|
||||
return groupData
|
||||
} catch (error) {
|
||||
console.error('Error getting Token group data', error)
|
||||
throw error
|
||||
}
|
||||
|
||||
return groupData
|
||||
}
|
||||
|
||||
// Given an IPFS URI, this will download and parse the JSON data.
|
||||
async getIpfsData (ipfsUri) {
|
||||
const cid = ipfsUri.slice(7)
|
||||
try {
|
||||
const cid = ipfsUri.slice(7)
|
||||
|
||||
const downloadUrl = `https://${cid}.ipfs.dweb.link/data.json`
|
||||
const downloadUrl = `https://${cid}.ipfs.dweb.link/data.json`
|
||||
|
||||
const response = await axios.get(downloadUrl)
|
||||
const data = response.data
|
||||
const response = await axios.get(downloadUrl)
|
||||
const data = response.data
|
||||
|
||||
return data
|
||||
return data
|
||||
} catch (error) {
|
||||
console.error('Error getting IPFS data', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Get a list of alternative back end servers.
|
||||
|
||||
@@ -8,6 +8,30 @@ class AppUtil {
|
||||
sleep (ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
// Copy a text to clipboard
|
||||
async copyToClipboard (text) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
} catch (err) {
|
||||
console.error('Failed to copy text:', err)
|
||||
// document.body.removeChild(textarea)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Read text from clipboard
|
||||
async readFromClipboard () {
|
||||
try {
|
||||
const text = await navigator.clipboard.readText()
|
||||
|
||||
return text
|
||||
} catch (err) {
|
||||
console.error('Failed to copy text:', err)
|
||||
// document.body.removeChild(textarea)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default AppUtil
|
||||
|
||||
Reference in New Issue
Block a user