Skip to content

Commit

Permalink
feat: first declarative utils [LIVE-9138] (#265)
Browse files Browse the repository at this point in the history
* feat: first declarative utils

* test: add RTL to nextjs example
add setTimeout in the simulator transport to better simulate async calls and support RTL tests
cleanup declarativeHandlers call and issues with types
export declarativeHandlers function

* test: update test name
Justkant authored Nov 24, 2023

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent 5823fc8 commit 7d3cdca
Showing 28 changed files with 1,858 additions and 533 deletions.
2 changes: 1 addition & 1 deletion apps/wallet-api-tools/package.json
Original file line number Diff line number Diff line change
@@ -17,7 +17,7 @@
"@radix-ui/react-icons": "^1.3.0",
"@tailwindcss/typography": "^0.5.9",
"@types/node": "20.8.7",
"@types/react": "18.2.28",
"@types/react": "18.2.38",
"@types/react-dom": "18.2.13",
"@uiw/codemirror-extensions-langs": "^4.21.20",
"@uiw/react-codemirror": "^4.21.20",
15 changes: 15 additions & 0 deletions examples/client-nextjs/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"root": true,
"extends": ["next/core-web-vitals"],
"plugins": ["testing-library"],
"overrides": [
// Only uses Testing Library lint rules in test files
{
"files": [
"**/__tests__/**/*.[jt]s?(x)",
"**/?(*.)+(spec|test).[jt]s?(x)"
],
"extends": ["plugin:testing-library/react"]
}
]
}
36 changes: 36 additions & 0 deletions examples/client-nextjs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env*.local

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
3 changes: 3 additions & 0 deletions examples/client-nextjs/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"plugins": ["prettier-plugin-tailwindcss"]
}
36 changes: 36 additions & 0 deletions examples/client-nextjs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).

## Getting Started

First, run the development server:

```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.

This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.

## Learn More

To learn more about Next.js, take a look at the following resources:

- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.

You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!

## Deploy on Vercel

The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.

Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
Binary file added examples/client-nextjs/app/favicon.ico
Binary file not shown.
27 changes: 27 additions & 0 deletions examples/client-nextjs/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

:root {
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255;
}

@media (prefers-color-scheme: dark) {
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
}
}

body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(
to bottom,
transparent,
rgb(var(--background-end-rgb))
)
rgb(var(--background-start-rgb));
}
19 changes: 19 additions & 0 deletions examples/client-nextjs/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import type { PropsWithChildren } from "react";
import "./globals.css";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};

export default function RootLayout({ children }: PropsWithChildren) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
);
}
41 changes: 41 additions & 0 deletions examples/client-nextjs/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"use client";

import { WindowMessageTransport } from "@ledgerhq/wallet-api-client";
import { WalletAPIProvider } from "@ledgerhq/wallet-api-client-react";
import {
getSimulatorTransport,
profiles,
} from "@ledgerhq/wallet-api-simulator";
import { AccountsList } from "../components/AccountsList";

const isSimulator =
typeof window === "undefined"
? false
: new URLSearchParams(window.location.search).get("simulator");

function getWalletAPITransport() {
if (typeof window === "undefined") {
return {
onMessage: undefined,
send: () => {},
};
}

if (isSimulator) {
return getSimulatorTransport(profiles.STANDARD);
}

const transport = new WindowMessageTransport();
transport.connect();
return transport;
}

const transport = getWalletAPITransport();

export default function Page() {
return (
<WalletAPIProvider transport={transport}>
<AccountsList />
</WalletAPIProvider>
);
}
62 changes: 62 additions & 0 deletions examples/client-nextjs/components/AccountsList.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { WalletAPIProvider } from "@ledgerhq/wallet-api-client-react";
import {
declarativeHandlers,
getSimulatorTransport,
profiles,
} from "@ledgerhq/wallet-api-simulator";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { PropsWithChildren } from "react";
import { AccountsList } from "./AccountsList";

const transport = getSimulatorTransport({
config: profiles.STANDARD.config,
permissions: {
currencyIds: ["ethereum", "bitcoin"],
methodIds: ["account.list", "message.sign"],
},
accounts: profiles.STANDARD.accounts,
currencies: profiles.STANDARD.currencies,
methods: declarativeHandlers({
"message.sign": [
// First call to message.sign and fallback after the third call
Buffer.from("0x123456789123456789"),
// Second call to message.sign only
({ account, message, meta }) => {
console.log(account);
console.log(message);
console.log(meta);
return message;
},
// Third call to message.sign only with an error
({ account, message, meta }) => {
console.log(account);
console.log(message);
console.log(meta);
throw new Error("Sign declined");
},
],
}),
});

const Providers = ({ children }: PropsWithChildren) => {
return (
<WalletAPIProvider transport={transport}>{children}</WalletAPIProvider>
);
};

it("should get and show accounts", async () => {
const user = userEvent.setup();
render(<AccountsList />, { wrapper: Providers });
expect(screen.getByRole("heading")).toHaveTextContent(
"Get started by editing app/page.tsx",
);
screen.debug();
expect(screen.getByRole("status")).toHaveTextContent("Loading accounts");
await user.click(screen.getByRole("button"));
screen.debug();
await waitFor(() => expect(screen.getByRole("status")).toBeEmptyDOMElement());
// Should add a check for the list of accounts
});

// it("should allow to sign")
69 changes: 69 additions & 0 deletions examples/client-nextjs/components/AccountsList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { useAccounts } from "@ledgerhq/wallet-api-client-react";

export function AccountsList() {
const accountsData = useAccounts();

return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
<div className="z-10 w-full max-w-5xl items-center justify-between font-mono text-sm lg:flex">
<p
role="heading"
className="fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30"
>
Get started by editing&nbsp;
<code className="font-mono font-bold">app/page.tsx</code>
</p>
</div>

<div className="relative flex w-full place-items-center justify-center before:absolute before:-z-[1] before:h-[300px] before:w-[480px] before:-translate-x-1/2 before:rounded-full before:bg-gradient-radial before:from-white before:to-transparent before:blur-2xl before:content-[''] after:absolute after:-z-20 after:h-[180px] after:w-[240px] after:translate-x-1/3 after:bg-gradient-conic after:from-sky-200 after:via-blue-200 after:blur-2xl after:content-[''] before:dark:bg-gradient-to-br before:dark:from-transparent before:dark:to-blue-700 before:dark:opacity-10 after:dark:from-sky-900 after:dark:via-[#0141ff] after:dark:opacity-40 before:lg:h-[360px]">
<div className="grow flex-col">
<button
type="button"
onClick={accountsData.updateData}
className="mb-2 me-2 rounded-lg bg-blue-700 px-5 py-2.5 text-sm font-medium text-white hover:bg-blue-800 focus:outline-none focus:ring-4 focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
>
Update accounts data
</button>
<p role="status">
{accountsData.loading ? "Loading accounts" : null}
</p>
<p>
{accountsData.updatedAt
? `Accounts updated at: ${accountsData.updatedAt.toLocaleTimeString()}`
: "Never updated yet"}
</p>
<ul>
{accountsData.accounts?.map((account) => {
return (
<li key={account.id}>
<button
className="m-4 flex w-full justify-around rounded-xl border border-gray-300 bg-gradient-to-b from-zinc-200 p-4 backdrop-blur-2xl hover:bg-zinc-700/30 dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:bg-gray-200 lg:dark:bg-zinc-800/30"
type="button"
>
<div>
<p>Name: {account.name}</p>
<p>Address: {account.address}</p>
</div>
<div>
<p>Balance: {account.balance.toString()}</p>
<p>Block Height: {account.blockHeight}</p>
</div>
</button>
</li>
);
})}
</ul>
{accountsData.error ? (
<div>
<pre>{JSON.stringify(accountsData.error, null, 2)}</pre>
</div>
) : null}
</div>
</div>

<div className="mb-32 grid text-center lg:mb-0 lg:w-full lg:max-w-5xl lg:grid-cols-4 lg:text-left">
Bottom
</div>
</main>
);
}
16 changes: 16 additions & 0 deletions examples/client-nextjs/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { Config } from "jest";
const nextJest = require("next/jest");

const createJestConfig = nextJest({
// Provide the path to your Next.js app to load next.config.js and .env files in your test environment
dir: "./",
});

// Add any custom config to be passed to Jest
const customJestConfig: Config = {
setupFilesAfterEnv: ["<rootDir>/jest.setup.ts"],
testEnvironment: "jest-environment-jsdom",
};

// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
module.exports = createJestConfig(customJestConfig);
2 changes: 2 additions & 0 deletions examples/client-nextjs/jest.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Learn more: https://github.com/testing-library/jest-dom
import "@testing-library/jest-dom";
4 changes: 4 additions & 0 deletions examples/client-nextjs/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/** @type {import('next').NextConfig} */
const nextConfig = {}

module.exports = nextConfig
40 changes: 40 additions & 0 deletions examples/client-nextjs/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"name": "client-nextjs",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"test": "jest"
},
"dependencies": {
"@ledgerhq/wallet-api-client": "workspace:*",
"@ledgerhq/wallet-api-client-react": "workspace:*",
"@ledgerhq/wallet-api-simulator": "workspace:*",
"next": "14.0.3",
"react": "^18",
"react-dom": "^18"
},
"devDependencies": {
"@testing-library/jest-dom": "^6.1.4",
"@testing-library/react": "^14.1.2",
"@testing-library/user-event": "^14.5.1",
"@types/jest": "^29.5.4",
"@types/node": "^20",
"@types/react": "^18.2.38",
"@types/react-dom": "^18",
"autoprefixer": "^10.0.1",
"eslint": "^8",
"eslint-config-next": "14.0.3",
"eslint-plugin-testing-library": "^6.2.0",
"jest": "^29.6.4",
"jest-environment-jsdom": "^29.6.4",
"postcss": "^8",
"prettier": "^3.0.3",
"prettier-plugin-tailwindcss": "^0.5.7",
"tailwindcss": "^3.3.0",
"typescript": "^5"
}
}
6 changes: 6 additions & 0 deletions examples/client-nextjs/postcss.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
1 change: 1 addition & 0 deletions examples/client-nextjs/public/next.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions examples/client-nextjs/public/vercel.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 20 additions & 0 deletions examples/client-nextjs/tailwind.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { Config } from 'tailwindcss'

const config: Config = {
content: [
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}',
'./app/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {
backgroundImage: {
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
'gradient-conic':
'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
},
},
},
plugins: [],
}
export default config
32 changes: 32 additions & 0 deletions examples/client-nextjs/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
]
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
"types.d.ts",
"./jest-setup.ts"
],
"exclude": ["node_modules"]
}
6 changes: 6 additions & 0 deletions examples/client-nextjs/types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
declare module "*module.css" {
const styles: {
[className: string]: string;
};
export default styles;
}
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -4,14 +4,15 @@
"private": true,
"workspaces": [
"packages/*",
"apps/*"
"apps/*",
"examples/*"
],
"scripts": {
"clean": "git clean -fdX",
"changelog": "changeset add",
"build": "turbo run build",
"build:client": "turbo run build --filter=wallet-api-client",
"dev": "turbo run dev",
"dev": "turbo run dev --filter=!./examples/*",
"lint": "turbo run lint",
"lint:fix": "turbo run lint:fix",
"test": "turbo run test",
2 changes: 1 addition & 1 deletion packages/client-react/package.json
Original file line number Diff line number Diff line change
@@ -25,7 +25,7 @@
"devDependencies": {
"@types/jest": "^29.5.4",
"@types/node": "^20.8.7",
"@types/react": "^18.2.21",
"@types/react": "^18.2.38",
"eslint": "^8.48.0",
"jest": "^29.6.4",
"jest-environment-jsdom": "^29.6.4",
2 changes: 1 addition & 1 deletion packages/server/package.json
Original file line number Diff line number Diff line change
@@ -21,7 +21,7 @@
"@types/jest": "^29.5.4",
"@types/node": "^20.8.7",
"@types/picomatch": "^2.3.0",
"@types/react": "^18.2.21",
"@types/react": "^18.2.38",
"eslint": "^8.48.0",
"jest": "^29.6.4",
"jest-environment-jsdom": "^29.6.4",
55 changes: 54 additions & 1 deletion packages/simulator/src/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import type { WalletAPIServer } from "@ledgerhq/wallet-api-server";
import type {
WalletAPIServer,
WalletHandlers,
} from "@ledgerhq/wallet-api-server";
import type { SimulatorProfile } from "./types";

export function applyProfile(
@@ -10,3 +13,53 @@ export function applyProfile(
serverInstance.setPermissions(profile.permissions);
serverInstance.setHandlers(profile.methods);
}

type MockedResponse<
K extends keyof WalletHandlers,
H extends WalletHandlers[K] = WalletHandlers[K],
> = ReturnType<H> | H;

export function declarativeHandler<K extends keyof WalletHandlers>(
mocks: MockedResponse<K>[],
): WalletHandlers[K] {
let numCalls = 0;

// @ts-expect-error: issue with types
return (...args) => {
// Finding the mock matching with the number of calls
// Or fallback to the first mock
const mock = numCalls > mocks.length ? mocks[0] : mocks[numCalls];

numCalls += 1;

if (!mock) {
return Promise.reject(new Error("No mock object found"));
}

if (typeof mock === "function") {
// @ts-expect-error: issue with types
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
return mock(...args);
}

return mock;
};
}

export type MockedHandlers = {
[K in keyof Partial<WalletHandlers>]: MockedResponse<K>[];
};

export function declarativeHandlers(
mocks: MockedHandlers,
): Partial<WalletHandlers> {
const handlers = {};

for (const key in mocks) {
// @ts-expect-error: issue with types
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
handlers[key] = declarativeHandler(mocks[key]);
}

return handlers;
}
27 changes: 17 additions & 10 deletions packages/simulator/src/transport.ts
Original file line number Diff line number Diff line change
@@ -7,30 +7,37 @@ import {
import { applyProfile } from "./helpers";
import type { SimulatorProfile } from "./types";

export { declarativeHandlers } from "./helpers";

export function getSimulatorTransport(
profile: SimulatorProfile,
customHandlers?: CustomHandlers,
): Transport {
// eslint-disable-next-line prefer-const
let clientTransport: Transport | undefined;

const serverTransport: Transport = {
onMessage: undefined,
send: (payload) => {
console.info("wallet -> app", payload);
if (clientTransport?.onMessage) {
clientTransport.onMessage(payload);
}
// Using setTimeout to simulate async call (Do we want to keep this sync ?)
// It also avoids an act warning when using RTL to test components
setTimeout(() => {
if (clientTransport.onMessage) {
clientTransport.onMessage(payload);
}
}, 0);
},
};

clientTransport = {
const clientTransport: Transport = {
onMessage: undefined,
send: (payload) => {
console.info("app -> wallet", payload);
if (serverTransport?.onMessage) {
serverTransport.onMessage(payload);
}
// Using setTimeout to simulate async call (Do we want to keep this sync ?)
// It also avoids an act warning when using RTL to test components
setTimeout(() => {
if (serverTransport.onMessage) {
serverTransport.onMessage(payload);
}
}, 0);
},
};

1,861 changes: 1,344 additions & 517 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
packages:
- "packages/*"
- "apps/*"
- "examples/*"

2 comments on commit 7d3cdca

@vercel
Copy link

@vercel vercel bot commented on 7d3cdca Nov 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

wallet-api-wallet-api-tools – ./apps/wallet-api-tools

wallet-api-wallet-api-tools.vercel.app
wallet-api-wallet-api-tools-git-main-ledgerhq.vercel.app
wallet-api-wallet-api-tools-ledgerhq.vercel.app

@vercel
Copy link

@vercel vercel bot commented on 7d3cdca Nov 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

wallet-api – ./apps/docs

wallet-api-ledgerhq.vercel.app
wallet-api-git-main-ledgerhq.vercel.app
wallet.api.live.ledger.com

Please sign in to comment.