Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: use-inkathon flipper frontend example #59

Merged
merged 2 commits into from
Feb 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions flipper/frontend/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}
3 changes: 3 additions & 0 deletions flipper/frontend/.gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

Expand Down
21 changes: 19 additions & 2 deletions flipper/frontend/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,21 @@
# Have Questions?
# ink! Frontend Example

For any questions about building front end applications with [useink](https://use.ink/frontend/overview/), join the [Element chat](https://matrix.to/#/%23useink:parity.io).
This is a vanilla [vite + typescript](https://vitejs.dev/) project to showcase the use of [`useinkathon`](https://github.com/scio-labs/use-inkathon).

## Getting Started

You can use the package manager of your choice to install the dependencies and start the project in development mode. We like `pnpm` right now. But this example should work with `npm` & `yarn` as well.

```sh
pnpm install
pnpm dev
```

## Change the Code

The actual interaction with the contract is all contained in the `./src/App.tsx` file. Every other file in the folder is only relevant for styling and bundling.


## Demo

<img src="demo.gif" width="600px" />
Binary file added flipper/frontend/demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion flipper/frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ink! Examples</title>
</head>
Expand Down
37 changes: 21 additions & 16 deletions flipper/frontend/package.json
Original file line number Diff line number Diff line change
@@ -1,30 +1,35 @@
{
"name": "flipper",
"name": "flipper-frontend-example",
"private": true,
"version": "0.1.0",
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"ui": "workspace:ui@*"
"@polkadot/api-contract": "^10.11.2",
"@polkadot/util-crypto": "^12.6.2",
"@scio-labs/use-inkathon": "^0.6.3",
"@tanstack/react-query": "^5.17.19",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.0.37",
"@types/react-dom": "^18.0.11",
"@typescript-eslint/eslint-plugin": "^5.59.0",
"@typescript-eslint/parser": "^5.59.0",
"@vitejs/plugin-react": "^4.0.0",
"autoprefixer": "^10.4.14",
"eslint": "^8.38.0",
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18",
"@typescript-eslint/eslint-plugin": "^6.19.1",
"@typescript-eslint/parser": "^6.19.1",
"@vitejs/plugin-react-swc": "^3.5.0",
"autoprefixer": "^10.4.17",
"eslint": "^8.56.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.3.4",
"postcss": "^8.4.24",
"tailwindcss": "^3.3.2",
"typescript": "^5.0.2",
"vite": "^4.5.2"
"eslint-plugin-react-refresh": "^0.4.5",
"postcss": "^8.4.33",
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3",
"vite": "^5.0.12"
}
}
2 changes: 1 addition & 1 deletion flipper/frontend/postcss.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ export default {
tailwindcss: {},
autoprefixer: {},
},
};
}
1 change: 0 additions & 1 deletion flipper/frontend/public/logo.svg

This file was deleted.

255 changes: 214 additions & 41 deletions flipper/frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,49 +1,222 @@
import { Button, Card, ConnectButton, InkLayout, formatContractName } from 'ui';
import { useCallSubscription, useContract, useTx, useWallet } from 'useink';
import { useTxNotifications } from 'useink/notifications';
import { pickDecoded, shouldDisable } from 'useink/utils';
import metadata from './assets/flipper.json';
import { CONTRACT_ROCOCO_ADDRESS } from './constants';
import {
SubstrateDeployment,
UseInkathonProvider,
contractQuery,
contractTx,
decodeOutput,
rococo,
useBalance,
useInkathon,
useRegisteredContract,
} from "@scio-labs/use-inkathon";
import {
QueryClient,
QueryClientProvider,
useMutation,
useQuery,
} from "@tanstack/react-query";

function App() {
const { account } = useWallet();
const contract = useContract(CONTRACT_ROCOCO_ADDRESS, metadata);
const getSub = useCallSubscription<boolean>(contract, 'get', [], {
defaultCaller: true,
});
import CONTRACT_METADATA from "./flipper.json";
const CONTRACT_NAME = "flipper";
const queryClient = new QueryClient();

const flip = useTx(contract, 'flip');
useTxNotifications(flip);
const getDeployments = async (): Promise<SubstrateDeployment[]> => {
return [
{
contractId: CONTRACT_NAME,
networkId: rococo.network,
abi: CONTRACT_METADATA,
address: "5Fsk6oqWHJzMAQmkBTVzxxqZPPngLbHG48Tro3i53LC3quao",
},
];
};

export default function WrappedApp() {
return (
<InkLayout
className='md:py-12 md:p-6 p-4 h-screen flex items-center justify-center'
animationSrc='https://raw.githubusercontent.com/paritytech/ink-workshop/d819d10a35b2ac3d2bff4f77a96701a527b3ad3a/frontend/public/dark-sea-creatures.json'
>
<Card className='mx-auto p-6 flex flex-col w-full max-w-md backdrop-blur-sm bg-opacity-70'>
<h1 className='text-2xl font-bold'>
{formatContractName(metadata.contract.name)}
</h1>

<p className='mt-6'>
Flipped:{' '}
<b className='uppercase'>{pickDecoded(getSub.result)?.toString()}</b>
</p>

{account ? (
<Button
disabled={shouldDisable(flip)}
onClick={() => flip.signAndSend()}
className='mt-6'
>
{shouldDisable(flip) ? 'Flipping...' : 'Flip'}
</Button>
) : (
<ConnectButton className='mt-6' />
<QueryClientProvider client={queryClient}>
<UseInkathonProvider
appName="Flipper Frontend Example"
deployments={getDeployments()}
defaultChain={rococo}
>
<App />
</UseInkathonProvider>
</QueryClientProvider>
);
}

function App() {
const { isConnected } = useInkathon();
return (
<div className="w-screen h-screen flex justify-center p-4">
<div className="max-w-2xl flex flex-col gap-4 ">
<ConnectionState />
{isConnected && (
<>
<FlipperInteraction />
</>
)}
</Card>
</InkLayout>
</div>
</div>
);
}

export default App;
const ConnectionState = () => {
const {
connect,
disconnect,
isConnected,
activeChain,
activeAccount,
setActiveAccount,
accounts,
} = useInkathon();
const { contract } = useRegisteredContract(CONTRACT_NAME);
const balance = useBalance(activeAccount?.address, true);

if (!isConnected) {
return (
<div>
<button type="button" onClick={() => (connect ? connect() : undefined)}>
Connect
</button>
</div>
);
}

return (
<div className="card">
<div className="grid grid-rows gap-2 overflow-hidden">
{activeChain && (
<div>
<div className="text-sm text-slate-500">Chain</div>
<div className="text-lg font-semibold">{activeChain.name}</div>
</div>
)}

{activeAccount && accounts && (
<div>
<div className="text-sm text-slate-500">Active Account</div>

<select
value={activeAccount.address}
onChange={(v) => {
const selectedAccount = accounts.find(
(account) => account.address === v.target.value
);

if (selectedAccount && setActiveAccount)
setActiveAccount(selectedAccount);
}}
>
{accounts.map((account) => (
<option value={account.address} key={account.address}>
{account.name ? account.name : account.address}
</option>
))}
</select>
<div className="text-sm text-ellipsis overflow-hidden">
{activeAccount.address}
</div>
</div>
)}

{balance && (
<div>
<div className="text-sm text-slate-500">Account Balance</div>
<div className="text-lg font-semibold">
{balance.balanceFormatted}
</div>
<div className="text-sm text-ellipsis overflow-hidden">
<a href="https://use.ink/5.x/faucet">
Get Tokens from Testnet Faucet
</a>
</div>
</div>
)}

{contract && (
<div>
<div className="text-sm text-slate-500">Contract</div>
<div className="text-lg font-semibold text-ellipsis overflow-hidden">
{contract?.address.toHex()}
</div>
</div>
)}
</div>
<button type="button" onClick={disconnect}>
Disconnect
</button>
</div>
);
};

const FlipperInteraction = () => {
const { api, activeAccount } = useInkathon();
const { contract } = useRegisteredContract(CONTRACT_NAME);

const { data: flipState, refetch: refetchFlipState } = useQuery({
queryKey: ["flipper", "get"],
queryFn: async () => {
if (!api || !contract) throw Error("api or contract not available");
const outcome = await contractQuery(api, "", contract, "get", {}, []);
return decodeOutput(outcome, contract, "get");
},
enabled: !!api && !!contract,
});

const {
mutateAsync: flip,
isPending,
error,
data: flipResult,
} = useMutation({
mutationKey: ["flipper", "flip"],
mutationFn: async () => {
if (!contract) throw new Error("Contract not available");
if (!api) throw new Error("API not available");
if (!activeAccount) throw new Error("Account not available");

return contractTx(api, activeAccount.address, contract, "flip", {}, []);
},
onSuccess: () => {
refetchFlipState();
},
});

return (
<div className="card">
<div>
<h3 className="font-semibold text-lg">Flip </h3>
<p className="slate-500">Change contracts storage value</p>
</div>

<div className="flex flex-row justify-between items-center">
{flipState?.decodedOutput && (
<div className="flex flow-row gap-2">
<code>Flipper.get()</code>
<div className="font-bold">{flipState?.decodedOutput}</div>
</div>
)}

<button type="submit" disabled={isPending} onClick={() => flip()}>
{isPending ? "flipping..." : "Flipper.flip()"}
</button>
</div>

{error && (
<>
<hr />
<div className="error">{JSON.stringify(error)}</div>
</>
)}

{flipResult && !!flipResult.successEvent && (
<>
<hr />
<div className="success">Value Flipped!</div>
</>
)}
</div>
);
};
3 changes: 0 additions & 3 deletions flipper/frontend/src/Global.css

This file was deleted.

2 changes: 0 additions & 2 deletions flipper/frontend/src/constants.ts

This file was deleted.

Loading