WIP dapp integration
This commit is contained in:
parent
fc296cf59e
commit
fbf5ad0951
19
src/App.js
19
src/App.js
|
@ -2,8 +2,8 @@ import React, { Suspense } from 'react';
|
|||
import CssBaseline from '@material-ui/core/CssBaseline';
|
||||
import useMediaQuery from '@material-ui/core/useMediaQuery';
|
||||
import {
|
||||
unstable_createMuiStrictModeTheme as createMuiTheme,
|
||||
ThemeProvider,
|
||||
unstable_createMuiStrictModeTheme as createMuiTheme,
|
||||
} from '@material-ui/core/styles';
|
||||
import blue from '@material-ui/core/colors/blue';
|
||||
import NavigationFrame from './components/NavigationFrame';
|
||||
|
@ -12,8 +12,9 @@ import WalletPage from './WalletPage';
|
|||
import { WalletProvider } from './utils/wallet';
|
||||
import LoadingIndicator from './components/LoadingIndicator';
|
||||
import { SnackbarProvider } from 'notistack';
|
||||
import PopupPage from './PopupPage';
|
||||
|
||||
function App() {
|
||||
export default function App() {
|
||||
// TODO: add toggle for dark mode
|
||||
const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
|
||||
const theme = React.useMemo(
|
||||
|
@ -27,6 +28,11 @@ function App() {
|
|||
[prefersDarkMode],
|
||||
);
|
||||
|
||||
// Disallow rendering inside an iframe to prevent clickjacking.
|
||||
if (window.self !== window.top) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense fallback={<LoadingIndicator />}>
|
||||
<ThemeProvider theme={theme}>
|
||||
|
@ -36,7 +42,7 @@ function App() {
|
|||
<SnackbarProvider maxSnack={5} autoHideDuration={8000}>
|
||||
<NavigationFrame>
|
||||
<Suspense fallback={<LoadingIndicator />}>
|
||||
<WalletPage />
|
||||
<PageContents />
|
||||
</Suspense>
|
||||
</NavigationFrame>
|
||||
</SnackbarProvider>
|
||||
|
@ -47,4 +53,9 @@ function App() {
|
|||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
function PageContents() {
|
||||
if (window.opener) {
|
||||
return <PopupPage opener={window.opener} />;
|
||||
}
|
||||
return <WalletPage />;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,201 @@
|
|||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useWallet } from './utils/wallet';
|
||||
import { Typography } from '@material-ui/core';
|
||||
import Card from '@material-ui/core/Card';
|
||||
import CardContent from '@material-ui/core/CardContent';
|
||||
import CardActions from '@material-ui/core/CardActions';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import ImportExportIcon from '@material-ui/icons/ImportExport';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import assert from 'assert';
|
||||
import bs58 from 'bs58';
|
||||
import nacl from 'tweetnacl';
|
||||
|
||||
export default function PopupPage({ opener }) {
|
||||
const wallet = useWallet();
|
||||
|
||||
const origin = useMemo(() => {
|
||||
let params = new URLSearchParams(window.location.hash.slice(1));
|
||||
return params.get('origin');
|
||||
}, []);
|
||||
const postMessage = useCallback(
|
||||
(message) => {
|
||||
opener.postMessage({ jsonrpc: '2.0', ...message }, origin);
|
||||
},
|
||||
[opener, origin],
|
||||
);
|
||||
|
||||
const [connectedAccount, setConnectedAccount] = useState(null);
|
||||
const hasConnectedAccount = !!connectedAccount;
|
||||
const [requests, setRequests] = useState([]);
|
||||
|
||||
// Send a disconnect event if this window is closed, this component is
|
||||
// unmounted, or setConnectedAccount(null) is called.
|
||||
useEffect(() => {
|
||||
if (hasConnectedAccount) {
|
||||
function unloadHandler() {
|
||||
postMessage({ method: 'disconnected' });
|
||||
}
|
||||
window.addEventListener('beforeunload', unloadHandler);
|
||||
return () => {
|
||||
unloadHandler();
|
||||
window.removeEventListener('beforeunload', unloadHandler);
|
||||
};
|
||||
}
|
||||
}, [hasConnectedAccount, postMessage]);
|
||||
|
||||
// Disconnect if the user switches to a different wallet.
|
||||
useEffect(() => {
|
||||
if (
|
||||
connectedAccount &&
|
||||
!connectedAccount.publicKey.equals(wallet.account.publicKey)
|
||||
) {
|
||||
setConnectedAccount(null);
|
||||
}
|
||||
}, [connectedAccount, wallet]);
|
||||
|
||||
// Push requests from the parent window into a queue.
|
||||
useEffect(() => {
|
||||
function messageHandler(e) {
|
||||
if (e.origin === origin && e.source === window.opener) {
|
||||
if (e.data.method !== 'signTransaction') {
|
||||
postMessage({ error: 'Unsupported method', id: e.data.id });
|
||||
}
|
||||
setRequests((requests) => [...requests, e.data]);
|
||||
}
|
||||
}
|
||||
window.addEventListener('message', messageHandler);
|
||||
return () => window.removeEventListener('message', messageHandler);
|
||||
}, [origin, postMessage]);
|
||||
|
||||
// Switch focus to the parent window. This requires that the parent
|
||||
// runs `window.name = 'parent'` before opening the popup.
|
||||
function focusParent() {
|
||||
window.open('', 'parent');
|
||||
}
|
||||
|
||||
if (
|
||||
!connectedAccount ||
|
||||
!connectedAccount.publicKey.equals(wallet.account.publicKey)
|
||||
) {
|
||||
// Approve the parent page to connect to this wallet.
|
||||
function connect() {
|
||||
setConnectedAccount(wallet.account);
|
||||
postMessage({
|
||||
method: 'connected',
|
||||
params: { publicKey: wallet.account.publicKey.toBase58() },
|
||||
});
|
||||
focusParent();
|
||||
}
|
||||
|
||||
return <ApproveConnectionForm origin={origin} onApprove={connect} />;
|
||||
}
|
||||
|
||||
if (requests.length > 0) {
|
||||
const request = requests[0];
|
||||
assert(request.method === 'signTransaction');
|
||||
const message = bs58.decode(request.params.message);
|
||||
function sendSignature() {
|
||||
setRequests((requests) => requests.slice(1));
|
||||
postMessage({
|
||||
result: {
|
||||
signature: bs58.encode(
|
||||
nacl.sign.detached(message, wallet.account.secretKey),
|
||||
),
|
||||
publicKey: wallet.account.publicKey.toBase58(),
|
||||
},
|
||||
id: request.id,
|
||||
});
|
||||
if (requests.length === 1) {
|
||||
focusParent();
|
||||
}
|
||||
}
|
||||
function sendReject() {
|
||||
setRequests((requests) => requests.slice(1));
|
||||
postMessage({
|
||||
error: 'Transaction cancelled',
|
||||
id: request.id,
|
||||
});
|
||||
if (requests.length === 1) {
|
||||
focusParent();
|
||||
}
|
||||
}
|
||||
return (
|
||||
<ApproveSignatureForm
|
||||
origin={origin}
|
||||
message={message}
|
||||
onApprove={sendSignature}
|
||||
onReject={sendReject}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Typography>Please keep this window open in the background.</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
connection: {
|
||||
marginTop: theme.spacing(3),
|
||||
marginBottom: theme.spacing(3),
|
||||
textAlign: 'center',
|
||||
},
|
||||
transaction: {
|
||||
wordBreak: 'break-all',
|
||||
},
|
||||
actions: {
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
}));
|
||||
|
||||
function ApproveConnectionForm({ origin, onApprove }) {
|
||||
const wallet = useWallet();
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" component="h1" gutterBottom>
|
||||
Allow this site to access your Solana account?
|
||||
</Typography>
|
||||
<div className={classes.connection}>
|
||||
<Typography>{origin}</Typography>
|
||||
<ImportExportIcon fontSize="large" />
|
||||
<Typography>{wallet.account.publicKey.toBase58()}</Typography>
|
||||
</div>
|
||||
<Typography>Only connect with sites you trust.</Typography>
|
||||
</CardContent>
|
||||
<CardActions className={classes.actions}>
|
||||
<Button onClick={window.close}>Cancel</Button>
|
||||
<Button color="primary" onClick={onApprove}>
|
||||
Connect
|
||||
</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function ApproveSignatureForm({ origin, message, onApprove, onReject }) {
|
||||
const classes = useStyles();
|
||||
|
||||
// TODO: decode message
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" component="h1" gutterBottom>
|
||||
{origin} would like to send the following transaction:
|
||||
</Typography>
|
||||
<Typography className={classes.transaction}>
|
||||
{bs58.encode(message)}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
<CardActions className={classes.actions}>
|
||||
<Button onClick={onReject}>Cancel</Button>
|
||||
<Button color="primary" onClick={onApprove}>
|
||||
Approve
|
||||
</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
);
|
||||
}
|
|
@ -22,6 +22,8 @@ const useStyles = makeStyles((theme) => ({
|
|||
content: {
|
||||
paddingTop: theme.spacing(3),
|
||||
paddingBottom: theme.spacing(3),
|
||||
paddingLeft: theme.spacing(1),
|
||||
paddingRight: theme.spacing(1),
|
||||
},
|
||||
title: {
|
||||
flexGrow: 1,
|
||||
|
|
Loading…
Reference in New Issue