implement web-utils in web-extension

This commit is contained in:
Sebastian 2022-12-06 11:28:56 -03:00
parent de9f10cbf0
commit 51bbf08d24
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
59 changed files with 243 additions and 753 deletions

View File

@ -40,6 +40,7 @@ function getFilesInDirectory(startPath, regex) {
return result return result
} }
// eslint-disable-next-line no-undef
const BASE = process.cwd() const BASE = process.cwd()
const allTestFiles = getFilesInDirectory(path.join(BASE, 'src'), /.test.ts$/) const allTestFiles = getFilesInDirectory(path.join(BASE, 'src'), /.test.ts$/)
@ -67,7 +68,9 @@ while (!fs.existsSync(path.join(GIT_ROOT, '.git')) && GIT_ROOT !== '/') {
GIT_ROOT = path.join(GIT_ROOT, '../') GIT_ROOT = path.join(GIT_ROOT, '../')
} }
if (GIT_ROOT === '/') { if (GIT_ROOT === '/') {
// eslint-disable-next-line no-undef
console.log("not found") console.log("not found")
// eslint-disable-next-line no-undef
process.exit(1); process.exit(1);
} }
const GIT_HASH = GIT_ROOT === '/' ? undefined : git_hash() const GIT_HASH = GIT_ROOT === '/' ? undefined : git_hash()
@ -118,10 +121,12 @@ export const buildConfig = {
], ],
} }
esbuild await esbuild
.build(buildConfig) .build(buildConfig)
.catch((e) => { .catch((e) => {
// eslint-disable-next-line no-undef
console.log(e) console.log(e)
// eslint-disable-next-line no-undef
process.exit(1) process.exit(1)
}); });

View File

@ -1,67 +1,38 @@
#!/usr/bin/env node #!/usr/bin/env node
/* eslint-disable no-undef */ /*
This file is part of GNU Taler
(C) 2022 Taler Systems S.A.
import linaria from '@linaria/esbuild' GNU Taler is free software; you can redistribute it and/or modify it under the
import esbuild from 'esbuild' terms of the GNU General Public License as published by the Free Software
import { buildConfig } from "./build-fast-with-linaria.mjs" Foundation; either version 3, or (at your option) any later version.
import fs from 'fs';
import WebSocket from "ws";
import chokidar from "chokidar";
import path from "path"
const devServerBroadcastDelay = 500 GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
const devServerPort = 8002 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
const wss = new WebSocket.Server({ port: devServerPort }); A PARTICULAR PURPOSE. See the GNU General Public License for more details.
const toWatch = ["./src"]
function broadcast(file, event) { You should have received a copy of the GNU General Public License along with
setTimeout(() => { GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
wss.clients.forEach((client) => { */
if (client.readyState === WebSocket.OPEN) {
console.log(new Date(), file)
client.send(JSON.stringify(event));
}
});
}, devServerBroadcastDelay);
}
const watcher = chokidar import { serve } from "@gnu-taler/web-util/lib/index.node";
.watch(toWatch, { import esbuild from 'esbuild';
persistent: true, import { buildConfig } from "./build-fast-with-linaria.mjs";
ignoreInitial: true,
awaitWriteFinish: { buildConfig.inject = ['./node_modules/@gnu-taler/web-util/lib/live-reload.mjs']
stabilityThreshold: 100,
pollInterval: 100, serve({
}, folder: './dist',
port: 8080,
source: './src',
development: true,
onUpdate: async () => esbuild.build(buildConfig)
}) })
.on("error", (error) => console.error(error))
.on("change", async (file) => {
broadcast(file, { type: "RELOAD" });
})
.on("add", async (file) => {
broadcast(file, { type: "RELOAD" });
})
.on("unlink", async (file) => {
broadcast(file, { type: "RELOAD" });
});
// FIXME: create a mocha test in the browser as it was before
fs.writeFileSync("dev-html/manifest.json", fs.readFileSync("manifest-v2.json")) // fs.writeFileSync("dev-html/manifest.json", fs.readFileSync("manifest-v2.json"))
fs.writeFileSync("dev-html/mocha.css", fs.readFileSync("node_modules/mocha/mocha.css")) // fs.writeFileSync("dev-html/mocha.css", fs.readFileSync("node_modules/mocha/mocha.css"))
fs.writeFileSync("dev-html/mocha.js", fs.readFileSync("node_modules/mocha/mocha.js")) // fs.writeFileSync("dev-html/mocha.js", fs.readFileSync("node_modules/mocha/mocha.js"))
fs.writeFileSync("dev-html/mocha.js.map", fs.readFileSync("node_modules/mocha/mocha.js.map")) // fs.writeFileSync("dev-html/mocha.js.map", fs.readFileSync("node_modules/mocha/mocha.js.map"))
const server = await esbuild
.serve({ servedir: 'dev-html' }, {
...buildConfig, outdir: 'dev-html/dist'
})
.catch((e) => {
console.log(e)
process.exit(1)
});
console.log(`Dev server is ready at http://localhost:${server.port}/.
http://localhost:${server.port}/stories.html for the components stories.
The server is running a using websocket at ${devServerPort} to notify code change and live reload.
`);

View File

@ -25,12 +25,11 @@
"@gnu-taler/taler-wallet-core": "workspace:*", "@gnu-taler/taler-wallet-core": "workspace:*",
"date-fns": "^2.29.2", "date-fns": "^2.29.2",
"history": "4.10.1", "history": "4.10.1",
"preact": "^10.6.5", "preact": "10.11.3",
"preact-router": "3.2.1", "preact-router": "3.2.1",
"qr-scanner": "^1.4.1", "qr-scanner": "^1.4.1",
"qrcode-generator": "^1.4.4", "qrcode-generator": "^1.4.4",
"tslib": "^2.4.0", "tslib": "^2.4.0"
"ws": "7.4.5"
}, },
"eslintConfig": { "eslintConfig": {
"plugins": [ "plugins": [
@ -44,6 +43,7 @@
} }
}, },
"devDependencies": { "devDependencies": {
"@gnu-taler/web-util": "workspace:*",
"@babel/core": "7.18.9", "@babel/core": "7.18.9",
"@babel/plugin-transform-modules-commonjs": "7.18.6", "@babel/plugin-transform-modules-commonjs": "7.18.6",
"@babel/plugin-transform-react-jsx-source": "7.18.6", "@babel/plugin-transform-react-jsx-source": "7.18.6",
@ -54,8 +54,6 @@
"@linaria/core": "3.0.0-beta.22", "@linaria/core": "3.0.0-beta.22",
"@linaria/react": "3.0.0-beta.22", "@linaria/react": "3.0.0-beta.22",
"@linaria/webpack-loader": "3.0.0-beta.22", "@linaria/webpack-loader": "3.0.0-beta.22",
"@testing-library/preact": "^2.0.1",
"@testing-library/preact-hooks": "^1.1.0",
"@types/chai": "^4.3.0", "@types/chai": "^4.3.0",
"@types/chrome": "0.0.197", "@types/chrome": "0.0.197",
"@types/history": "^4.7.8", "@types/history": "^4.7.8",
@ -64,7 +62,6 @@
"babel-loader": "^8.2.3", "babel-loader": "^8.2.3",
"babel-plugin-transform-react-jsx": "^6.24.1", "babel-plugin-transform-react-jsx": "^6.24.1",
"chai": "^4.3.6", "chai": "^4.3.6",
"chokidar": "^3.5.3",
"esbuild": "^0.15.13", "esbuild": "^0.15.13",
"mocha": "^9.2.0", "mocha": "^9.2.0",
"nyc": "^15.1.0", "nyc": "^15.1.0",

View File

@ -24,7 +24,7 @@ import { Fragment, h, VNode } from "preact";
import { Amount } from "./Amount.js"; import { Amount } from "./Amount.js";
export default { export default {
title: "components/amount", title: "amount",
component: Amount, component: Amount,
}; };

View File

@ -29,7 +29,7 @@ import { AmountFieldHandler, TextFieldHandler } from "../mui/handlers.js";
import { AmountField } from "./AmountField.js"; import { AmountField } from "./AmountField.js";
export default { export default {
title: "components/amountField", title: "amountField",
}; };
function RenderAmount(): VNode { function RenderAmount(): VNode {

View File

@ -26,7 +26,7 @@ import { Banner } from "./Banner.js";
import { SvgIcon } from "./styled/index.js"; import { SvgIcon } from "./styled/index.js";
import wifiIcon from "../svg/wifi.svg"; import wifiIcon from "../svg/wifi.svg";
export default { export default {
title: "mui/banner", title: "banner",
component: Banner, component: Banner,
}; };

View File

@ -28,7 +28,7 @@ import { createExample } from "../test-utils.js";
import { PendingTransactionsView as TestedComponent } from "./PendingTransactions.js"; import { PendingTransactionsView as TestedComponent } from "./PendingTransactions.js";
export default { export default {
title: "component/PendingTransactions", title: "PendingTransactions",
component: TestedComponent, component: TestedComponent,
}; };

View File

@ -23,7 +23,7 @@ import { createExample } from "../test-utils.js";
import { QR } from "./QR.js"; import { QR } from "./QR.js";
export default { export default {
title: "wallet/qr", title: "qr",
}; };
export const Restore = createExample(QR, { export const Restore = createExample(QR, {

View File

@ -29,7 +29,7 @@ import {
} from "./ShowFullContractTermPopup.js"; } from "./ShowFullContractTermPopup.js";
export default { export default {
title: "component/ShowFullContractTermPopup", title: "ShowFullContractTermPopup",
}; };
const cd: WalletContractData = { const cd: WalletContractData = {

View File

@ -19,12 +19,10 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import * as a1 from "./Banner.stories.js"; export * as a1 from "./Banner.stories.js";
import * as a2 from "./PendingTransactions.stories.js"; export * as a2 from "./PendingTransactions.stories.js";
import * as a3 from "./Amount.stories.js"; export * as a3 from "./Amount.stories.js";
import * as a4 from "./ShowFullContractTermPopup.stories.js"; export * as a4 from "./ShowFullContractTermPopup.stories.js";
import * as a5 from "./TermsOfService/stories.js"; export * as a5 from "./TermsOfService/stories.js";
import * as a6 from "./QR.stories"; export * as a6 from "./QR.stories";
import * as a7 from "./AmountField.stories.js"; export * as a7 from "./AmountField.stories.js";
export default [a1, a2, a3, a4, a5, a6, a7];

View File

@ -24,7 +24,7 @@ import { createExample } from "../../test-utils.js";
import { ReadyView } from "./views.js"; import { ReadyView } from "./views.js";
export default { export default {
title: "cta/deposit", title: "deposit",
}; };
export const Ready = createExample(ReadyView, { export const Ready = createExample(ReadyView, {

View File

@ -23,7 +23,7 @@ import { createExample } from "../../test-utils.js";
import { ReadyView } from "./views.js"; import { ReadyView } from "./views.js";
export default { export default {
title: "wallet/invoice create", title: "invoice create",
}; };
export const Ready = createExample(ReadyView, { export const Ready = createExample(ReadyView, {

View File

@ -24,7 +24,7 @@ import { createExample } from "../../test-utils.js";
import { ReadyView } from "./views.js"; import { ReadyView } from "./views.js";
export default { export default {
title: "wallet/invoice payment", title: "invoice payment",
}; };
export const Ready = createExample(ReadyView, { export const Ready = createExample(ReadyView, {

View File

@ -30,7 +30,7 @@ import { BaseView } from "./views.js";
import beer from "../../../static-dev/beer.png"; import beer from "../../../static-dev/beer.png";
export default { export default {
title: "cta/payment", title: "payment",
component: BaseView, component: BaseView,
argTypes: {}, argTypes: {},
}; };

View File

@ -24,5 +24,5 @@ import { createExample } from "../../test-utils.js";
import { ReadyView } from "./views.js"; import { ReadyView } from "./views.js";
export default { export default {
title: "cta/recovery", title: "recovery",
}; };

View File

@ -24,7 +24,7 @@ import beer from "../../../static-dev/beer.png";
import { createExample } from "../../test-utils.js"; import { createExample } from "../../test-utils.js";
import { IgnoredView, InProgressView, ReadyView } from "./views.js"; import { IgnoredView, InProgressView, ReadyView } from "./views.js";
export default { export default {
title: "cta/refund", title: "refund",
}; };
export const InProgress = createExample(InProgressView, { export const InProgress = createExample(InProgressView, {

View File

@ -24,7 +24,7 @@ import { createExample } from "../../test-utils.js";
import { AcceptedView, ReadyView } from "./views.js"; import { AcceptedView, ReadyView } from "./views.js";
export default { export default {
title: "cta/tip", title: "tip",
}; };
export const Accepted = createExample(AcceptedView, { export const Accepted = createExample(AcceptedView, {

View File

@ -23,7 +23,7 @@ import { createExample } from "../../test-utils.js";
import { ReadyView } from "./views.js"; import { ReadyView } from "./views.js";
export default { export default {
title: "wallet/transfer create", title: "transfer create",
}; };
export const Ready = createExample(ReadyView, { export const Ready = createExample(ReadyView, {

View File

@ -23,7 +23,7 @@ import { createExample } from "../../test-utils.js";
import { ReadyView } from "./views.js"; import { ReadyView } from "./views.js";
export default { export default {
title: "wallet/transfer pickup", title: "transfer pickup",
}; };
export const Ready = createExample(ReadyView, { export const Ready = createExample(ReadyView, {

View File

@ -25,7 +25,7 @@ import { createExample } from "../../test-utils.js";
import { SuccessView } from "./views.js"; import { SuccessView } from "./views.js";
export default { export default {
title: "cta/withdraw", title: "withdraw",
}; };
const exchangeList = { const exchangeList = {

View File

@ -19,14 +19,12 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import * as a1 from "./Deposit/stories.jsx"; export * as a1 from "./Deposit/stories.jsx";
import * as a3 from "./Payment/stories.jsx"; export * as a3 from "./Payment/stories.jsx";
import * as a4 from "./Refund/stories.jsx"; export * as a4 from "./Refund/stories.jsx";
import * as a5 from "./Tip/stories.jsx"; export * as a5 from "./Tip/stories.jsx";
import * as a6 from "./Withdraw/stories.jsx"; export * as a6 from "./Withdraw/stories.jsx";
import * as a8 from "./InvoiceCreate/stories.js"; export * as a8 from "./InvoiceCreate/stories.js";
import * as a9 from "./InvoicePay/stories.js"; export * as a9 from "./InvoicePay/stories.js";
import * as a10 from "./TransferCreate/stories.js"; export * as a10 from "./TransferCreate/stories.js";
import * as a11 from "./TransferPickup/stories.js"; export * as a11 from "./TransferPickup/stories.js";
export default [a1, a3, a4, a5, a6, a8, a9, a10, a11];

View File

@ -24,7 +24,7 @@ import { ComponentChildren, Fragment, h, VNode } from "preact";
import { Alert } from "./Alert.jsx"; import { Alert } from "./Alert.jsx";
export default { export default {
title: "mui/alert", title: "alert",
component: Alert, component: Alert,
}; };

View File

@ -26,8 +26,7 @@ import SendIcon from "../svg/send_24px.svg";
import { styled } from "@linaria/react"; import { styled } from "@linaria/react";
export default { export default {
title: "mui/button", title: "Button",
component: Button,
}; };
const Stack = styled.div` const Stack = styled.div`

View File

@ -23,7 +23,7 @@ import { Grid } from "./Grid.js";
import { Fragment, h, VNode } from "preact"; import { Fragment, h, VNode } from "preact";
export default { export default {
title: "mui/grid", title: "grid",
component: Grid, component: Grid,
}; };

View File

@ -25,7 +25,7 @@ import { Menu, MenuItem } from "./Menu.jsx";
import { Paper } from "./Paper.js"; import { Paper } from "./Paper.js";
export default { export default {
title: "mui/menu", title: "menu",
component: Menu, component: Menu,
}; };

View File

@ -23,7 +23,7 @@ import { h, VNode } from "preact";
import { Paper } from "./Paper.js"; import { Paper } from "./Paper.js";
export default { export default {
title: "mui/paper", title: "paper",
component: Paper, component: Paper,
}; };

View File

@ -25,7 +25,7 @@ import { useState } from "preact/hooks";
import { TextField, Props } from "./TextField.js"; import { TextField, Props } from "./TextField.js";
export default { export default {
title: "mui/TextField", title: "TextField",
component: TextField, component: TextField,
}; };

View File

@ -19,11 +19,9 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import * as a1 from "./Button.stories.js"; export * as a1 from "./Button.stories.js";
import * as a3 from "./Grid.stories.js"; export * as a3 from "./Grid.stories.js";
import * as a4 from "./Paper.stories.js"; export * as a4 from "./Paper.stories.js";
import * as a5 from "./TextField.stories.js"; export * as a5 from "./TextField.stories.js";
import * as a6 from "./Alert.stories.js"; export * as a6 from "./Alert.stories.js";
import * as a7 from "./Menu.stories.js"; export * as a7 from "./Menu.stories.js";
export default [a1, a3, a4, a5, a6, a7];

View File

@ -23,9 +23,7 @@ import { createExample } from "../test-utils.js";
import { BalanceView as TestedComponent } from "./BalancePage.js"; import { BalanceView as TestedComponent } from "./BalancePage.js";
export default { export default {
title: "popup/balance", title: "balance",
component: TestedComponent,
argTypes: {},
}; };
export const EmptyBalance = createExample(TestedComponent, { export const EmptyBalance = createExample(TestedComponent, {

View File

@ -23,8 +23,7 @@ import { createExample } from "../test-utils.js";
import { TalerActionFound as TestedComponent } from "./TalerActionFound.js"; import { TalerActionFound as TestedComponent } from "./TalerActionFound.js";
export default { export default {
title: "popup/TalerActionFound", title: "TalerActionFound",
component: TestedComponent,
}; };
export const PayAction = createExample(TestedComponent, { export const PayAction = createExample(TestedComponent, {

View File

@ -19,7 +19,5 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import * as a2 from "./Balance.stories.js"; export * as a1 from "./Balance.stories.js";
import * as a6 from "./TalerActionFound.stories.js"; export * as a2 from "./TalerActionFound.stories.js";
export default [a2, a6];

View File

@ -19,37 +19,33 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { setupI18n } from "@gnu-taler/taler-util"; import { setupI18n } from "@gnu-taler/taler-util";
import { parseGroupImport } from "@gnu-taler/web-util/lib/index.browser";
import { setupPlatform } from "./platform/api.js";
import chromeAPI from "./platform/chrome.js";
import { renderNodeOrBrowser } from "./test-utils.js";
import * as components from "./components/index.stories.js"; import * as components from "./components/index.stories.js";
import * as cta from "./cta/index.stories.js"; import * as cta from "./cta/index.stories.js";
import * as mui from "./mui/index.stories.js"; import * as mui from "./mui/index.stories.js";
import { setupPlatform } from "./platform/api.js";
import chromeAPI from "./platform/chrome.js";
import * as popup from "./popup/index.stories.js"; import * as popup from "./popup/index.stories.js";
import { renderNodeOrBrowser } from "./test-utils.js";
import * as wallet from "./wallet/index.stories.js"; import * as wallet from "./wallet/index.stories.js";
setupI18n("en", { en: {} }); setupI18n("en", { en: {} });
setupPlatform(chromeAPI); setupPlatform(chromeAPI);
function testThisStory(st: any): any { describe("All the examples:", () => {
describe(`example "${(st as any).default.title}"`, () => { const cms = parseGroupImport({ popup, wallet, cta, mui, components })
Object.keys(st).forEach((k) => { cms.forEach(group => {
const Component = (st as any)[k]; describe(`Example for group "${group.title}:"`, () => {
if (k === "default" || !Component) return; group.list.forEach(component => {
describe(`Component ${component.name}:`, () => {
it(`example: ${k}`, () => { component.examples.forEach(example => {
renderNodeOrBrowser(Component, Component.args); it(`should render example: ${example.name}`, () => {
}); renderNodeOrBrowser(example.render.component, example.render.props)
}); })
}); })
} })
})
describe("render every storybook example", () => { })
[popup, wallet, cta, mui, components].forEach(function testAll(st: any) { })
if (Array.isArray(st.default)) {
st.default.forEach(testAll);
} else {
testThisStory(st);
}
});
}); });

View File

@ -18,432 +18,27 @@
* *
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { setupI18n } from "@gnu-taler/taler-util"; import { Fragment, FunctionComponent, h } from "preact";
import { styled } from "@linaria/react";
import {
ComponentChild,
ComponentChildren,
Fragment,
FunctionComponent,
h,
render,
VNode,
} from "preact";
import { useEffect, useErrorBoundary, useState } from "preact/hooks";
import { LogoHeader } from "./components/LogoHeader.js"; import { LogoHeader } from "./components/LogoHeader.js";
import { PopupBox, WalletBox } from "./components/styled/index.js"; import { PopupBox, WalletBox } from "./components/styled/index.js";
import * as mui from "./mui/index.stories.js"; import { strings } from "./i18n/strings.js";
import { PopupNavBar, WalletNavBar } from "./NavigationBar.js"; import { PopupNavBar, WalletNavBar } from "./NavigationBar.js";
import * as components from "./components/index.stories.js";
import * as cta from "./cta/index.stories.js";
import * as mui from "./mui/index.stories.js";
import * as popup from "./popup/index.stories.js"; import * as popup from "./popup/index.stories.js";
import * as wallet from "./wallet/index.stories.js"; import * as wallet from "./wallet/index.stories.js";
import * as cta from "./cta/index.stories.js";
import * as components from "./components/index.stories.js";
import { strings } from "./i18n/strings.js";
import { setupPlatform } from "./platform/api.js";
import chromeAPI from "./platform/chrome.js";
import firefoxAPI from "./platform/firefox.js";
const url = new URL(window.location.href); import { renderStories } from "@gnu-taler/web-util/lib/index.browser";
const lang = url.searchParams.get("lang") || "en";
setupI18n(lang, strings); function main(): void {
renderStories(
const Page = styled.div` { popup, wallet, cta, mui, components },
* { {
font-family: Arial, Helvetica, sans-serif; strings,
} getWrapperForGroup,
p:not([class]) { },
margin-bottom: 1em;
margin-top: 1em;
}
width: 100%;
display: flex;
flex-direction: row;
`;
const SideBar = styled.div`
min-width: var(--with-size);
height: calc(100vh - 20px);
overflow-y: visible;
overflow-x: hidden;
scroll-behavior: smooth;
* {
margin: 0px;
padding: 0px;
}
& > {
ol {
padding: 4px;
div:first-child {
background-color: lightcoral;
cursor: pointer;
}
div[data-hide="true"] {
display: none;
}
dd {
margin-left: 1em;
padding: 4px;
cursor: pointer;
border-radius: 4px;
margin-bottom: 4px;
}
dd:nth-child(even) {
background-color: lightgray;
}
dd:nth-child(odd) {
background-color: lightblue;
}
a {
color: black;
}
dd[data-selected] {
background-color: green;
}
}
}
`;
const ResizeHandleDiv = styled.div`
width: 10px;
background: #ddd;
cursor: ew-resize;
`;
function ResizeHandle({ onUpdate }: { onUpdate: (x: number) => void }): VNode {
const [start, setStart] = useState<number | undefined>(undefined);
return (
<ResizeHandleDiv
onMouseDown={(e: any) => {
setStart(e.pageX);
console.log("active", e.pageX);
return false;
}}
onMouseMove={(e: any) => {
if (start !== undefined) {
onUpdate(e.pageX - start);
}
return false;
}}
onMouseUp={() => {
setStart(undefined);
return false;
}}
/>
);
}
const Content = styled.div`
width: 100%;
padding: 20px;
`;
function parseExampleImport(group: string, im: any): ComponentItem {
const component = im.default.title;
return {
name: component,
examples: Object.entries(im)
.filter(([k]) => k !== "default")
.map(
([name, render]) =>
({
group,
component,
name,
render,
} as ExampleItem),
),
};
}
const allExamples = Object.entries({ popup, wallet, cta, mui, components }).map(
([title, value]) => ({
title,
list: value.default.map((s) => parseExampleImport(title, s)),
}),
);
interface ComponentItem {
name: string;
examples: ExampleItem[];
}
interface ExampleItem {
group: string;
component: string;
name: string;
render: {
(args: any): VNode;
args: any;
};
}
function findByGroupComponentName(
group: string,
component: string,
name: string,
): ExampleItem | undefined {
const gl = allExamples.filter((e) => e.title === group);
if (gl.length === 0) {
return undefined;
}
const cl = gl[0].list.filter((l) => l.name === component);
if (cl.length === 0) {
return undefined;
}
const el = cl[0].examples.filter((c) => c.name === name);
if (el.length === 0) {
return undefined;
}
return el[0];
}
function getContentForExample(item: ExampleItem | undefined): () => VNode {
if (!item)
return function SelectExampleMessage() {
return <div>select example from the list on the left</div>;
};
const example = findByGroupComponentName(
item.group,
item.component,
item.name,
);
if (!example) {
return function ExampleNotFoundMessage() {
return <div>example not found</div>;
};
}
return () => example.render(example.render.args);
}
function ExampleList({
name,
list,
selected,
onSelectStory,
}: {
name: string;
list: {
name: string;
examples: ExampleItem[];
}[];
selected: ExampleItem | undefined;
onSelectStory: (i: ExampleItem, id: string) => void;
}): VNode {
const [isOpen, setOpen] = useState(selected && selected.group === name);
return (
<ol>
<div onClick={() => setOpen(!isOpen)}>{name}</div>
<div data-hide={!isOpen}>
{list.map((k) => (
<li key={k.name}>
<dl>
<dt>{k.name.substring(k.name.indexOf("/") + 1)}</dt>
{k.examples.map((r) => {
const e = encodeURIComponent;
const eId = `${e(r.group)}-${e(r.component)}-${e(r.name)}`;
const isSelected =
selected &&
selected.component === r.component &&
selected.group === r.group &&
selected.name === r.name;
return (
<dd id={eId} key={r.name} data-selected={isSelected}>
<a
href={`#${eId}`}
onClick={(e) => {
e.preventDefault();
location.hash = `#${eId}`;
onSelectStory(r, eId);
history.pushState({}, "", `#${eId}`);
}}
>
{r.name}
</a>
</dd>
);
})}
</dl>
</li>
))}
</div>
</ol>
);
}
/**
* Prevents the UI from redirecting and inform the dev
* where the <a /> should have redirected
* @returns
*/
function PreventLinkNavigation({
children,
}: {
children: ComponentChildren;
}): VNode {
return (
<div
onClick={(e) => {
let t: any = e.target;
do {
if (t.localName === "a" && t.getAttribute("href")) {
alert(`should navigate to: ${t.attributes.href.value}`);
e.stopImmediatePropagation();
e.stopPropagation();
e.preventDefault();
return false;
}
} while ((t = t.parentNode));
}}
>
{children}
</div>
);
}
function getWrapperForGroup(group: string): FunctionComponent {
switch (group) {
case "popup":
return function PopupWrapper({ children }: any) {
return (
<PreventLinkNavigation>
<PopupNavBar />
<PopupBox>{children}</PopupBox>
</PreventLinkNavigation>
);
};
case "wallet":
return function WalletWrapper({ children }: any) {
return (
<PreventLinkNavigation>
<LogoHeader />
<WalletNavBar />
<WalletBox>{children}</WalletBox>
</PreventLinkNavigation>
);
};
case "cta":
return function WalletWrapper({ children }: any) {
return (
<PreventLinkNavigation>
<WalletBox>{children}</WalletBox>
</PreventLinkNavigation>
);
};
default:
return Fragment;
}
}
function ErrorReport({
children,
selected,
}: {
children: ComponentChild;
selected: ExampleItem | undefined;
}): VNode {
const [error, resetError] = useErrorBoundary();
//if there is an error, reset when unloading this component
useEffect(() => (error ? resetError : undefined));
if (error) {
return (
<div>
<p>Error was thrown trying to render</p>
{selected && (
<ul>
<li>
<b>group</b>: {selected.group}
</li>
<li>
<b>component</b>: {selected.component}
</li>
<li>
<b>example</b>: {selected.name}
</li>
<li>
<b>args</b>:{" "}
<pre>{JSON.stringify(selected.render.args, undefined, 2)}</pre>
</li>
</ul>
)}
<p>{error.message}</p>
<pre>{error.stack}</pre>
</div>
);
}
return <Fragment>{children}</Fragment>;
}
function getSelectionFromLocationHash(hash: string): ExampleItem | undefined {
if (!hash) return undefined;
const parts = hash.substring(1).split("-");
if (parts.length < 3) return undefined;
return findByGroupComponentName(
decodeURIComponent(parts[0]),
decodeURIComponent(parts[1]),
decodeURIComponent(parts[2]),
);
}
function Application(): VNode {
const initialSelection = getSelectionFromLocationHash(location.hash);
const [selected, updateSelected] = useState<ExampleItem | undefined>(
initialSelection,
);
useEffect(() => {
if (location.hash) {
const hash = location.hash.substring(1);
const found = document.getElementById(hash);
if (found) {
setTimeout(() => {
found.scrollIntoView({
block: "center",
});
}, 10);
}
}
}, []);
const ExampleContent = getContentForExample(selected);
const GroupWrapper = getWrapperForGroup(selected?.group || "default");
const [sidebarWidth, setSidebarWidth] = useState(200);
return (
<Page>
<LiveReload />
<SideBar style={{ "--with-size": `${sidebarWidth}px` }}>
{allExamples.map((e) => (
<ExampleList
key={e.title}
name={e.title}
list={e.list}
selected={selected}
onSelectStory={(item, htmlId) => {
document.getElementById(htmlId)?.scrollIntoView({
block: "center",
});
updateSelected(item);
}}
/>
))}
<hr />
</SideBar>
<ResizeHandle
onUpdate={(x) => {
setSidebarWidth((s) => s + x);
}}
/>
<Content>
<ErrorReport selected={selected}>
<GroupWrapper>
<ExampleContent />
</GroupWrapper>
</ErrorReport>
</Content>
</Page>
); );
} }
@ -452,82 +47,36 @@ if (document.readyState === "loading") {
} else { } else {
main(); main();
} }
function main(): void { function getWrapperForGroup(group: string): FunctionComponent {
try { switch (group) {
const container = document.getElementById("container"); case "popup":
if (!container) { return function PopupWrapper({ children }: any) {
throw Error("container not found, can't mount page contents");
}
render(<Application />, container);
} catch (e) {
console.error("got error", e);
if (e instanceof Error) {
document.body.innerText = `Fatal error: "${e.message}". Please report this bug at https://bugs.gnunet.org/.`;
}
}
}
let liveReloadMounted = false;
function LiveReload({ port = 8002 }: { port?: number }): VNode {
const [isReloading, setIsReloading] = useState(false);
useEffect(() => {
if (!liveReloadMounted) {
setupLiveReload(port, () => {
setIsReloading(true);
window.location.reload();
});
liveReloadMounted = true;
}
});
if (isReloading) {
return ( return (
<div <Fragment>
style={{ <PopupNavBar />
position: "absolute", <PopupBox>{children}</PopupBox>
width: "100%", </Fragment>
height: "100%",
backgroundColor: "rgba(0,0,0,0.5)",
color: "white",
display: "flex",
justifyContent: "center",
}}
>
<h1 style={{ margin: "auto" }}>reloading...</h1>
</div>
); );
}
return <Fragment />;
}
function setupLiveReload(port: number, onReload: () => void): void {
const protocol = location.protocol === "https:" ? "wss:" : "ws:";
const host = location.hostname;
const socketPath = `${protocol}//${host}:${port}/socket`;
const ws = new WebSocket(socketPath);
ws.onmessage = (message) => {
const event = JSON.parse(message.data);
if (event.type === "LOG") {
console.log(event.message);
}
if (event.type === "RELOAD") {
onReload();
}
}; };
ws.onerror = (error) => { case "wallet":
console.error(error); return function WalletWrapper({ children }: any) {
return (
<Fragment>
<LogoHeader />
<WalletNavBar />
<WalletBox>{children}</WalletBox>
</Fragment>
);
}; };
case "cta":
return function WalletWrapper({ children }: any) {
return (
<Fragment>
<WalletBox>{children}</WalletBox>
</Fragment>
);
};
default:
return Fragment;
} }
const isFirefox = typeof (window as any)["InstallTrigger"] !== "undefined";
//FIXME: create different entry point for any platform instead of
//switching in runtime
if (isFirefox) {
console.log("Wallet setup for Firefox API");
setupPlatform(firefoxAPI);
} else {
console.log("Wallet setup for Chrome API");
setupPlatform(chromeAPI);
} }

View File

@ -49,8 +49,12 @@ export function createExample<Props>(
// check how we can build evaluatedProps in render time // check how we can build evaluatedProps in render time
const evaluatedProps = typeof props === "function" ? props() : props; const evaluatedProps = typeof props === "function" ? props() : props;
const Render = (args: any): VNode => create(Component, args); const Render = (args: any): VNode => create(Component, args);
Render.args = evaluatedProps; // Render.args = evaluatedProps;
return Render;
return {
component: Render,
props: evaluatedProps
};
} }
export function createExampleWithCustomContext<Props, ContextProps>( export function createExampleWithCustomContext<Props, ContextProps>(
@ -66,8 +70,11 @@ export function createExampleWithCustomContext<Props, ContextProps>(
...contextProps, ...contextProps,
children: [Render(args)], children: [Render(args)],
} as any); } as any);
WithContext.args = evaluatedProps;
return WithContext; return {
component: WithContext,
props: evaluatedProps
};
} }
export function NullLink({ export function NullLink({

View File

@ -23,7 +23,7 @@ import { createExample } from "../../test-utils.js";
import { ConfirmProviderView, SelectProviderView } from "./views.js"; import { ConfirmProviderView, SelectProviderView } from "./views.js";
export default { export default {
title: "wallet/backup/confirm", title: "add backup provider",
}; };
export const DemoService = createExample(ConfirmProviderView, { export const DemoService = createExample(ConfirmProviderView, {

View File

@ -23,7 +23,7 @@ import { createExample } from "../test-utils.js";
import { AddNewActionView as TestedComponent } from "./AddNewActionView.js"; import { AddNewActionView as TestedComponent } from "./AddNewActionView.js";
export default { export default {
title: "wallet/add new action", title: "add new action",
component: TestedComponent, component: TestedComponent,
argTypes: { argTypes: {
setDeviceName: () => Promise.resolve(), setDeviceName: () => Promise.resolve(),

View File

@ -29,13 +29,7 @@ import { createExample } from "../test-utils.js";
import { TalerProtocolTimestamp } from "@gnu-taler/taler-util"; import { TalerProtocolTimestamp } from "@gnu-taler/taler-util";
export default { export default {
title: "wallet/backup/list", title: "backup",
component: TestedComponent,
argTypes: {
onRetry: { action: "onRetry" },
onDelete: { action: "onDelete" },
onBack: { action: "onBack" },
},
}; };
export const LotOfProviders = createExample(TestedComponent, { export const LotOfProviders = createExample(TestedComponent, {

View File

@ -25,7 +25,7 @@ import { labelForAccountType } from "./state.js";
import { ReadyView } from "./views.js"; import { ReadyView } from "./views.js";
export default { export default {
title: "wallet/deposit", title: "deposit",
}; };
// const ac = parsePaytoUri("payto://iban/ES8877998399652238")!; // const ac = parsePaytoUri("payto://iban/ES8877998399652238")!;

View File

@ -74,7 +74,7 @@ export namespace State {
} }
export type Contact = { export type Contact = {
icon: string; icon_type: string;
name: string; name: string;
description: string; description: string;
}; };

View File

@ -21,7 +21,6 @@ import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import { assertUnreachable, RecursiveState } from "../../utils/index.js"; import { assertUnreachable, RecursiveState } from "../../utils/index.js";
import { wxApi } from "../../wxApi.js"; import { wxApi } from "../../wxApi.js";
import { Contact, Props, State } from "./index.js"; import { Contact, Props, State } from "./index.js";
import bankIcon from "../../svg/ri-bank-line.svg";
export function useComponentState( export function useComponentState(
props: Props, props: Props,
@ -44,17 +43,17 @@ export function useComponentState(
: [ : [
{ {
name: "International Bank", name: "International Bank",
icon: bankIcon, //FIXME: should be decided in the view icon_type: 'bank',
description: "account ending with 3454", description: "account ending with 3454",
}, },
{ {
name: "Max", name: "Max",
icon: bankIcon, icon_type: 'bank',
description: "account ending with 3454", description: "account ending with 3454",
}, },
{ {
name: "Alex", name: "Alex",
icon: bankIcon, icon_type: 'bank',
description: "account ending with 3454", description: "account ending with 3454",
}, },
]; ];

View File

@ -23,7 +23,7 @@ import { createExample } from "../../test-utils.js";
import { ReadyView, SelectCurrencyView } from "./views.js"; import { ReadyView, SelectCurrencyView } from "./views.js";
export default { export default {
title: "wallet/destination", title: "destination",
}; };
export const GetCash = createExample(ReadyView, { export const GetCash = createExample(ReadyView, {

View File

@ -377,6 +377,7 @@ export function ReadySendView({
</Container> </Container>
); );
} }
import bankIcon from "../../svg/ri-bank-line.svg";
function RowExample({ function RowExample({
info, info,
@ -385,15 +386,22 @@ function RowExample({
info: Contact; info: Contact;
disabled?: boolean; disabled?: boolean;
}): VNode { }): VNode {
const icon = info.icon_type === "bank" ? bankIcon : undefined;
return ( return (
<MediaExample data-disabled={disabled}> <MediaExample data-disabled={disabled}>
<MediaLeft> <MediaLeft>
<CircleDiv> <CircleDiv>
{icon !== undefined ? (
<SvgIcon <SvgIcon
title={info.name} title={info.name}
dangerouslySetInnerHTML={{ __html: info.icon }} dangerouslySetInnerHTML={{
__html: icon,
}}
color="currentColor" color="currentColor"
/> />
) : (
<span>A</span>
)}
</CircleDiv> </CircleDiv>
</MediaLeft> </MediaLeft>
<MediaBody> <MediaBody>

View File

@ -24,7 +24,7 @@ import { createExample } from "../test-utils.js";
import { View as TestedComponent } from "./DeveloperPage.js"; import { View as TestedComponent } from "./DeveloperPage.js";
export default { export default {
title: "wallet/developer", title: "developer",
component: TestedComponent, component: TestedComponent,
argTypes: { argTypes: {
setDeviceName: () => Promise.resolve(), setDeviceName: () => Promise.resolve(),

View File

@ -23,7 +23,7 @@ import { createExample } from "../test-utils.js";
import { ExchangeAddConfirmPage as TestedComponent } from "./ExchangeAddConfirm.js"; import { ExchangeAddConfirmPage as TestedComponent } from "./ExchangeAddConfirm.js";
export default { export default {
title: "wallet/exchange add/confirm", title: "exchange add confirm",
component: TestedComponent, component: TestedComponent,
argTypes: { argTypes: {
onRetry: { action: "onRetry" }, onRetry: { action: "onRetry" },

View File

@ -24,13 +24,7 @@ import { queryToSlashKeys } from "../utils/index.js";
import { ExchangeSetUrlPage as TestedComponent } from "./ExchangeSetUrl.js"; import { ExchangeSetUrlPage as TestedComponent } from "./ExchangeSetUrl.js";
export default { export default {
title: "wallet/exchange add/set url", title: "exchange add set url",
component: TestedComponent,
argTypes: {
onRetry: { action: "onRetry" },
onDelete: { action: "onDelete" },
onBack: { action: "onBack" },
},
}; };
export const ExpectedUSD = createExample(TestedComponent, { export const ExpectedUSD = createExample(TestedComponent, {

View File

@ -23,7 +23,7 @@ import { createExample } from "../../test-utils.js";
import { ComparingView, ReadyView } from "./views.js"; import { ComparingView, ReadyView } from "./views.js";
export default { export default {
title: "wallet/select exchange", title: "select exchange",
}; };
export const Bitcoin1 = createExample(ReadyView, { export const Bitcoin1 = createExample(ReadyView, {

View File

@ -40,7 +40,7 @@ import { HistoryView as TestedComponent } from "./History.js";
import { createExample } from "../test-utils.js"; import { createExample } from "../test-utils.js";
export default { export default {
title: "wallet/balance", title: "balance",
component: TestedComponent, component: TestedComponent,
}; };

View File

@ -23,7 +23,7 @@ import { createExample } from "../../test-utils.js";
import { ReadyView } from "./views.js"; import { ReadyView } from "./views.js";
export default { export default {
title: "wallet/manage account", title: "manage account",
}; };
const nullFunction = async () => { const nullFunction = async () => {

View File

@ -24,7 +24,7 @@ import { createExample } from "../../test-utils.js";
import { ReadyView } from "./views.js"; import { ReadyView } from "./views.js";
export default { export default {
title: "wallet/notifications", title: "notifications",
}; };
export const Ready = createExample(ReadyView, { export const Ready = createExample(ReadyView, {

View File

@ -23,7 +23,7 @@ import { createExample } from "../test-utils.js";
import { ConfirmProviderView as TestedComponent } from "./ProviderAddPage.js"; import { ConfirmProviderView as TestedComponent } from "./ProviderAddPage.js";
export default { export default {
title: "wallet/backup/confirm", title: "confirm",
component: TestedComponent, component: TestedComponent,
argTypes: { argTypes: {
onRetry: { action: "onRetry" }, onRetry: { action: "onRetry" },

View File

@ -23,7 +23,7 @@ import { createExample } from "../test-utils.js";
import { SetUrlView as TestedComponent } from "./ProviderAddPage.js"; import { SetUrlView as TestedComponent } from "./ProviderAddPage.js";
export default { export default {
title: "wallet/backup/add", title: "add",
component: TestedComponent, component: TestedComponent,
argTypes: { argTypes: {
onRetry: { action: "onRetry" }, onRetry: { action: "onRetry" },

View File

@ -25,7 +25,7 @@ import { createExample } from "../test-utils.js";
import { ProviderView as TestedComponent } from "./ProviderDetailPage.js"; import { ProviderView as TestedComponent } from "./ProviderDetailPage.js";
export default { export default {
title: "wallet/backup/details", title: "provider details",
component: TestedComponent, component: TestedComponent,
argTypes: { argTypes: {
onRetry: { action: "onRetry" }, onRetry: { action: "onRetry" },

View File

@ -23,7 +23,7 @@ import { createExample } from "../test-utils.js";
import { QrReaderPage } from "./QrReader.js"; import { QrReaderPage } from "./QrReader.js";
export default { export default {
title: "wallet/qr reader", title: "qr reader",
}; };
export const Reading = createExample(QrReaderPage, {}); export const Reading = createExample(QrReaderPage, {});

View File

@ -24,7 +24,7 @@ import { createExample } from "../test-utils.js";
import { ReserveCreated as TestedComponent } from "./ReserveCreated.js"; import { ReserveCreated as TestedComponent } from "./ReserveCreated.js";
export default { export default {
title: "wallet/manual withdraw/reserve created", title: "reserve created",
component: TestedComponent, component: TestedComponent,
argTypes: {}, argTypes: {},
}; };

View File

@ -23,7 +23,7 @@ import { createExample } from "../test-utils.js";
import { SettingsView as TestedComponent } from "./Settings.js"; import { SettingsView as TestedComponent } from "./Settings.js";
export default { export default {
title: "wallet/settings", title: "settings",
component: TestedComponent, component: TestedComponent,
argTypes: { argTypes: {
setDeviceName: () => Promise.resolve(), setDeviceName: () => Promise.resolve(),

View File

@ -46,7 +46,7 @@ import {
import { TransactionView as TestedComponent } from "./Transaction.js"; import { TransactionView as TestedComponent } from "./Transaction.js";
export default { export default {
title: "wallet/history/details", title: "transaction details",
component: TestedComponent, component: TestedComponent,
argTypes: { argTypes: {
onRetry: { action: "onRetry" }, onRetry: { action: "onRetry" },

View File

@ -23,7 +23,7 @@ import { createExample } from "../test-utils.js";
import { View as TestedComponent } from "./Welcome.js"; import { View as TestedComponent } from "./Welcome.js";
export default { export default {
title: "wallet/welcome", title: "welcome",
component: TestedComponent, component: TestedComponent,
}; };

View File

@ -19,42 +19,21 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import * as a1 from "./Backup.stories.js"; export * as a1 from "./Backup.stories.js";
import * as a4 from "./DepositPage/stories.js"; export * as a4 from "./DepositPage/stories.js";
import * as a5 from "./ExchangeAddConfirm.stories.js"; export * as a5 from "./ExchangeAddConfirm.stories.js";
import * as a6 from "./ExchangeAddSetUrl.stories.js"; export * as a6 from "./ExchangeAddSetUrl.stories.js";
import * as a7 from "./History.stories.js"; export * as a7 from "./History.stories.js";
import * as a8 from "./AddBackupProvider/stories.js"; export * as a8 from "./AddBackupProvider/stories.js";
import * as a10 from "./ProviderDetail.stories.js"; export * as a10 from "./ProviderDetail.stories.js";
import * as a11 from "./ReserveCreated.stories.js"; export * as a11 from "./ReserveCreated.stories.js";
import * as a12 from "./Settings.stories.js"; export * as a12 from "./Settings.stories.js";
import * as a13 from "./Transaction.stories.js"; export * as a13 from "./Transaction.stories.js";
import * as a14 from "./Welcome.stories.js"; export * as a14 from "./Welcome.stories.js";
import * as a15 from "./AddNewActionView.stories.js"; export * as a15 from "./AddNewActionView.stories.js";
import * as a16 from "./DeveloperPage.stories.js"; export * as a16 from "./DeveloperPage.stories.js";
import * as a17 from "./QrReader.stories.js"; export * as a17 from "./QrReader.stories.js";
import * as a18 from "./DestinationSelection/stories.js"; export * as a18 from "./DestinationSelection/stories.js";
import * as a19 from "./ExchangeSelection/stories.js"; export * as a19 from "./ExchangeSelection/stories.js";
import * as a20 from "./ManageAccount/stories.js"; export * as a20 from "./ManageAccount/stories.js";
import * as a21 from "./Notifications/stories.js"; export * as a21 from "./Notifications/stories.js";
export default [
a1,
a4,
a5,
a6,
a7,
a8,
a10,
a11,
a12,
a13,
a14,
a15,
a16,
a17,
a18,
a19,
a20,
a21,
];

View File

@ -31,6 +31,9 @@
{ {
"path": "../taler-wallet-core/" "path": "../taler-wallet-core/"
}, },
{
"path": "../web-util/"
},
{ {
"path": "../taler-util/" "path": "../taler-util/"
} }