diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..c4645b4
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,9 @@
+# Copy to `.env` to override defaults. Only variables prefixed with REACT_APP_
+# are exposed to the app (Create React App convention).
+
+# Enable verbose debug/log output in production builds.
+# - During local development (`npm start`) logs are ON by default regardless.
+# - In production builds logs are OFF unless this is set to "true".
+# - You can also toggle logs at runtime from the browser console, without
+# rebuilding: dappLogs.on() / dappLogs.off() / dappLogs.toggle().
+REACT_APP_DEBUG=false
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 5509669..bce689b 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -22,6 +22,10 @@ jobs:
run: |
npm i
+ - name: Create production .env
+ run: |
+ echo "REACT_APP_DEBUG=false" > .env
+
- name: Build
run: |
npm run build
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..f309098
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,37 @@
+name: Test
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+
+permissions:
+ contents: read
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Read .nvmrc
+ id: nvm
+ run: echo "version=$(cat .nvmrc)" >> $GITHUB_OUTPUT
+
+ - name: Setup node
+ uses: actions/setup-node@v4
+ with:
+ node-version: '${{ steps.nvm.outputs.version }}'
+ cache: 'npm'
+
+ - name: Install dependencies
+ run: npm i
+
+ - name: Check formatting
+ run: npm run format:check
+
+ - name: Lint
+ run: npm run lint
+
+ - name: Run tests
+ run: npm run test:ci
diff --git a/.prettierrc b/.prettierrc
index 833bd64..99fa213 100644
--- a/.prettierrc
+++ b/.prettierrc
@@ -7,8 +7,5 @@
"semi": false,
"singleQuote": true,
"bracketSpacing": false,
- "arrowParens": "always",
- "importOrder": ["^@(.*)$", "^[./]" ],
- "importOrderSeparation": true,
- "importOrderSortSpecifiers": true
+ "arrowParens": "always"
}
diff --git a/README.md b/README.md
index e5be600..ab6f694 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,90 @@
# dapp-example
-The project is created with purpose to show how to work with yoroi dApp-connector.
+A simple, multi-chain example dApp that shows how to interact with wallet
+connectors — built for clarity, learning, testing, and extension.
-It may not look good but it works and can be used as an example to other projects how to interact with the dApp-connector.
+Live demo: https://dapp-example.yoroiwallet.com/
-You can find the dApp on this link https://dapp-example.yoroiwallet.com/
+> It may not look fancy, but it works and is meant as a reference for other
+> projects on how to talk to wallet dApp-connectors.
+
+## Supported chains
+
+- **Cardano** (CIP-30) — live, via browser wallet extensions (e.g. Yoroi)
+- **Ethereum** (EIP-1193) — live, via `window.ethereum`
+- **Bitcoin** — planned (provider is currently a stub)
+
+## Tech stack
+
+- React 18 (functional components + hooks), JavaScript only (no TypeScript)
+- Tailwind CSS + Material Tailwind
+- Create React App + craco
+- Cardano serialization via `@emurgo/cardano-serialization-lib-browser`
+
+## Getting started
+
+Prerequisites: Node.js (see [`.nvmrc`](./.nvmrc)) and npm.
+
+```bash
+npm install # install dependencies
+npm start # run the dev server at http://localhost:3000
+npm run build # production build into ./build
+```
+
+A Cardano and/or Ethereum browser wallet extension is required to exercise the
+connect flows.
+
+## Available scripts
+
+| Script | Description |
+| --- | --- |
+| `npm start` | Start the development server |
+| `npm run build` | Production build |
+| `npm test` | Run tests in watch mode |
+| `npm run test:ci` | Run tests once (CI mode) |
+| `npm run lint` | Lint `src` with ESLint |
+| `npm run lint:fix` | Lint and auto-fix |
+| `npm run format` | Format `src` with Prettier |
+| `npm run format:check` | Check formatting without writing |
+
+## Project structure
+
+```
+src/
+ hooks/ # Providers (one per chain) + network toggle + shared hooks
+ components/ # UI: access buttons, tabs, cards, shared inputs
+ utils/ # Helpers and blockchain utilities (cslTools, ethereumUtils, ...)
+```
+
+Each chain follows the same flow: **Provider → Access Button → Main Tab →
+Subtabs → Cards**. See [`CLAUDE.md`](./CLAUDE.md) for the full architecture and
+conventions.
+
+## Logging
+
+The app uses a small debug-gated logger (`src/utils/logger.js`):
+
+- `debug` / `log` / `info` print only in development, or in a production build
+ started with `REACT_APP_DEBUG=true` (see [`.env.example`](./.env.example)).
+- `warn` / `error` always print.
+
+You can toggle logs live from the browser console — no rebuild needed:
+
+```js
+dappLogs.on() // enable and remember across reloads
+dappLogs.off() // disable and remember across reloads
+dappLogs.toggle() // flip current state
+dappLogs.status() // -> true | false
+dappLogs.reset() // forget the override, fall back to the build default
+```
+
+## Testing
+
+Tests use Jest + React Testing Library (via CRA). Run `npm run test:ci` for a
+single pass, or `npm test` to watch. Tests and linting run in CI on pushes to
+`main` and on pull requests.
+
+## Configuration
+
+Copy [`.env.example`](./.env.example) to `.env` to override defaults. Only
+variables prefixed with `REACT_APP_` are exposed to the app.
diff --git a/package-lock.json b/package-lock.json
index a99e8c5..0e6f5fb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "dapp-example",
- "version": "2.3.0",
+ "version": "2.4.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "dapp-example",
- "version": "2.3.0",
+ "version": "2.4.0",
"dependencies": {
"@emurgo/cardano-serialization-lib-browser": "14.1.2",
"@emurgo/cip4-js": "^1.0.7",
@@ -35,6 +35,7 @@
"@rollup/plugin-terser": "^0.4.4",
"autoprefixer": "^10.4.8",
"postcss": "^8.4.16",
+ "prettier": "^3.9.4",
"tailwindcss": "^3.1.8"
}
},
@@ -18713,6 +18714,22 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/prettier": {
+ "version": "3.9.4",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.4.tgz",
+ "integrity": "sha512-yWG/o/4oJfo036EKAfK6ACAoDOfHeRHx4tuxkfBZiauURiaSmYwlpOr5LQqKtIkRD2z1PLteme2WoxEnj4tHTg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
"node_modules/pretty-bytes": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
diff --git a/package.json b/package.json
index c453dda..847b080 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "dapp-example",
- "version": "2.3.0",
+ "version": "2.4.0",
"private": true,
"dependencies": {
"@emurgo/cardano-serialization-lib-browser": "14.1.2",
@@ -30,12 +30,18 @@
"@rollup/plugin-terser": "^0.4.4",
"autoprefixer": "^10.4.8",
"postcss": "^8.4.16",
+ "prettier": "^3.9.4",
"tailwindcss": "^3.1.8"
},
"scripts": {
"start": "craco start",
"build": "CI=false && craco build",
"test": "craco test",
+ "test:ci": "CI=true craco test",
+ "lint": "eslint src",
+ "lint:fix": "eslint src --fix",
+ "format": "prettier --write \"src/**/*.js\"",
+ "format:check": "prettier --check \"src/**/*.js\"",
"eject": "craco eject"
},
"eslintConfig": {
diff --git a/src/App.js b/src/App.js
index d0720c9..4d1f44a 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,8 +1,9 @@
+import logger from './utils/logger'
import React, {useEffect} from 'react'
import AccessButton from './components/accessButton'
import MainTab from './components/tabs/mainTab'
import TabsComponent from './components/tabs/tabsComponent'
-import useYoroi from './hooks/yoroiProvider'
+import useCardano from './hooks/cardanoProvider'
import useNetwork, {NETWORK_CARDANO, NETWORK_ETHEREUM} from './hooks/networkProvider'
import BitcoinAccessButton from './components/bitcoinAccessButton'
import BitcoinMainTab from './components/tabs/bitcoinMainTab'
@@ -24,7 +25,7 @@ import EthTransactionsTab from './components/tabs/subtabs/ethTransactionsTab'
import Erc20Tab from './components/tabs/subtabs/erc20Tab'
const App = () => {
- const {connectionState, selectedWallet, setConnectionState, setConnectionStateFalse} = useYoroi()
+ const {connectionState, selectedWallet, setConnectionState, setConnectionStateFalse} = useCardano()
const {activeNetwork} = useNetwork()
const isWalletConnected = connectionState === CONNECTED
const isNoProvider = connectionState === NO_PROVIDER
@@ -42,7 +43,7 @@ const App = () => {
useEffect(() => {
const getConnectionState = async () => {
- console.debug(`[dApp][App] Checking connection works`)
+ logger.debug(`[dApp][App] Checking connection works`)
try {
const walletObject = window.cardano[selectedWallet]
const conState = await walletStateWithTimeout(walletObject, 10000)
@@ -54,14 +55,14 @@ const App = () => {
}
} catch (error) {
setConnectionStateFalse()
- console.error(error)
+ logger.error(error)
}
}
if (isWalletConnected) {
const connectionTimer = setInterval(getConnectionState, 10000)
return () => {
- console.debug(`[dApp][App] Checking connection is stopped`)
+ logger.debug(`[dApp][App] Checking connection is stopped`)
clearInterval(connectionTimer)
}
}
diff --git a/src/components/accessButton.js b/src/components/accessButton.js
index 27559fa..cc6be1c 100644
--- a/src/components/accessButton.js
+++ b/src/components/accessButton.js
@@ -1,11 +1,13 @@
+import logger from '../utils/logger'
import React from 'react'
-import useYoroi from '../hooks/yoroiProvider'
+import useCardano from '../hooks/cardanoProvider'
import {IN_PROGRESS} from '../utils/connectionStates'
import WalletsModal from './walletsModal'
+import AccessButtonShell from './accessButtonShell'
const AccessButton = () => {
- const {api, connectionState, availableWallets, selectedWallet} = useYoroi()
- console.log(`[dApp][AccessButton] available wallets: ${availableWallets.length}`)
+ const {api, connectionState, availableWallets, selectedWallet} = useCardano()
+ logger.log(`[dApp][AccessButton] available wallets: ${availableWallets.length}`)
const getWalletIcon = () => {
return window.cardano[selectedWallet].icon
@@ -19,27 +21,25 @@ const AccessButton = () => {
}
return (
-
-
- {api ? (
-
-
})
-
-
Connected To {getWalletName()}
-
anonymous wallet
-
+
+ {api ? (
+
+
})
+
+
Connected To {getWalletName()}
+
anonymous wallet
- ) : connectionState === IN_PROGRESS ? (
-
-
-
- ) : (
-
-
-
- )}
-
-
+
+ ) : connectionState === IN_PROGRESS ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
)
}
diff --git a/src/components/accessButtonShell.js b/src/components/accessButtonShell.js
new file mode 100644
index 0000000..b115e11
--- /dev/null
+++ b/src/components/accessButtonShell.js
@@ -0,0 +1,10 @@
+import React from 'react'
+
+// Shared chrome for each chain's access button: dark background + centered grid.
+const AccessButtonShell = ({children, innerClassName = 'grid justify-items-center py-3'}) => (
+
+)
+
+export default AccessButtonShell
diff --git a/src/components/bitcoinAccessButton.js b/src/components/bitcoinAccessButton.js
index 7dc02fb..9b162a2 100644
--- a/src/components/bitcoinAccessButton.js
+++ b/src/components/bitcoinAccessButton.js
@@ -1,18 +1,14 @@
-import React from 'react'
+import AccessButtonShell from './accessButtonShell'
-const BitcoinAccessButton = () => {
- return (
-
-
-
-
-
- )
-}
+const BitcoinAccessButton = () => (
+
+
+
+)
export default BitcoinAccessButton
diff --git a/src/components/cards/apiCard.js b/src/components/cards/apiCard.js
index 7d522ed..73a5d8b 100644
--- a/src/components/cards/apiCard.js
+++ b/src/components/cards/apiCard.js
@@ -1,5 +1,13 @@
import React from 'react'
+// Static map so Tailwind's JIT keeps these classes — `h-${height}` would be
+// built at runtime and purged from the production build.
+const HEIGHT_CLASSES = {
+ 10: 'h-10',
+ 16: 'h-16',
+ 24: 'h-24',
+}
+
const ApiCard = (props) => {
const {apiName, clickFunction, color, height} = props
@@ -7,7 +15,7 @@ const ApiCard = (props) => {
if (color != null) {
localColor = color
}
- const localHeight = height == null ? 'h-16' : `h-${height}`
+ const localHeight = HEIGHT_CLASSES[height] || 'h-16'
const localClassName = `w-full ${localHeight} ${localColor} disabled:bg-gray-800 rounded-lg text-white text-lg`
diff --git a/src/components/cards/apiCard.test.js b/src/components/cards/apiCard.test.js
new file mode 100644
index 0000000..4a1aee4
--- /dev/null
+++ b/src/components/cards/apiCard.test.js
@@ -0,0 +1,28 @@
+import {render, screen, fireEvent} from '@testing-library/react'
+import ApiCard from './apiCard'
+
+describe('ApiCard', () => {
+ it('renders the apiName as a button and fires clickFunction', () => {
+ const onClick = jest.fn()
+ render(
)
+
+ const button = screen.getByRole('button', {name: 'Do Thing'})
+ fireEvent.click(button)
+ expect(onClick).toHaveBeenCalledTimes(1)
+ })
+
+ it('applies a mapped height class', () => {
+ render(
{}} height={24} />)
+ expect(screen.getByRole('button', {name: 'Tall'}).className).toContain('h-24')
+ })
+
+ it('falls back to h-16 for an unmapped height', () => {
+ render( {}} height={999} />)
+ expect(screen.getByRole('button', {name: 'Default'}).className).toContain('h-16')
+ })
+
+ it('uses the provided color class when given', () => {
+ render( {}} color="bg-red-700" />)
+ expect(screen.getByRole('button', {name: 'Red'}).className).toContain('bg-red-700')
+ })
+})
diff --git a/src/components/cards/apiCardWithModal.js b/src/components/cards/apiCardWithModal.js
index 0c0cf43..519c7b2 100644
--- a/src/components/cards/apiCardWithModal.js
+++ b/src/components/cards/apiCardWithModal.js
@@ -1,13 +1,14 @@
+import logger from '../../utils/logger'
import Popup from 'reactjs-popup'
export const ApiCardWithModal = (props) => {
const {buttonLabel, clickFunction, halfOpacity, children, btnDisabled} = props
const handleActionAndClose = (closeFunc) => {
- console.log(`[dApp][ApiCardWithModal][${buttonLabel}] is called`)
+ logger.log(`[dApp][ApiCardWithModal][${buttonLabel}] is called`)
clickFunction()
closeFunc()
- console.log(`[dApp][ApiCardWithModal][${buttonLabel}] is closed`)
+ logger.log(`[dApp][ApiCardWithModal][${buttonLabel}] is closed`)
}
const overlayStyle = {background: 'rgba(0,0,0,0.75)'}
@@ -29,8 +30,8 @@ export const ApiCardWithModal = (props) => {
}
return (
- {buttonLabel}}
+ {buttonLabel}}
{...{modal, nested, overlayStyle}}
contentStyle={contentStyle}
>
@@ -53,16 +54,14 @@ export const ApiCardWithModal = (props) => {
{/* content section */}
-
- {children}
-
+ {children}
{/* end of content section*/}
{/* confirmation button */}