Skip to content

Commit

Permalink
feat: use-inkathon flipper frontend example (#59)
Browse files Browse the repository at this point in the history
* replaces ink flipper example with standalone basic use-inkathon example

* adds flipper frontend demo gif
  • Loading branch information
peetzweg authored Feb 23, 2024
1 parent 53213d8 commit 683c3d2
Show file tree
Hide file tree
Showing 19 changed files with 2,518 additions and 254 deletions.
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.

File renamed without changes.
Loading

0 comments on commit 683c3d2

Please sign in to comment.