This commit is contained in:
Sebastian 2022-06-06 00:09:25 -03:00
parent 912813fd09
commit fb6aff76d2
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
37 changed files with 4123 additions and 7225 deletions

View File

@ -27,17 +27,17 @@ import { platform, setupPlatform } from "./platform/api.js";
import devAPI from "./platform/dev.js"; import devAPI from "./platform/dev.js";
import { wxMain } from "./wxBackend.js"; import { wxMain } from "./wxBackend.js";
console.log("Wallet setup for Dev API") console.log("Wallet setup for Dev API");
setupPlatform(devAPI) setupPlatform(devAPI);
try { try {
platform.registerOnInstalled(() => { platform.registerOnInstalled(() => {
platform.openWalletPage("/welcome") platform.openWalletPage("/welcome");
}) });
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }
platform.notifyWhenAppIsReady(() => { platform.notifyWhenAppIsReady(() => {
wxMain(); wxMain();
}) });

View File

@ -28,22 +28,24 @@ import chromeAPI from "./platform/chrome.js";
import firefoxAPI from "./platform/firefox.js"; import firefoxAPI from "./platform/firefox.js";
import { wxMain } from "./wxBackend.js"; import { wxMain } from "./wxBackend.js";
const isFirefox = typeof (window as any) !== 'undefined' && typeof (window as any)['InstallTrigger'] !== 'undefined' const isFirefox =
typeof (window as any) !== "undefined" &&
typeof (window as any)["InstallTrigger"] !== "undefined";
// FIXME: create different entry point for any platform instead of // FIXME: create different entry point for any platform instead of
// switching in runtime // switching in runtime
if (isFirefox) { if (isFirefox) {
console.log("Wallet setup for Firefox API") console.log("Wallet setup for Firefox API");
setupPlatform(firefoxAPI) setupPlatform(firefoxAPI);
} else { } else {
console.log("Wallet setup for Chrome API") console.log("Wallet setup for Chrome API");
setupPlatform(chromeAPI) setupPlatform(chromeAPI);
} }
try { try {
platform.registerOnInstalled(() => { platform.registerOnInstalled(() => {
platform.openWalletPage("/welcome") platform.openWalletPage("/welcome");
}) });
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }
@ -51,4 +53,4 @@ try {
// setGlobalLogLevelFromString("trace") // setGlobalLogLevelFromString("trace")
platform.notifyWhenAppIsReady(() => { platform.notifyWhenAppIsReady(() => {
wxMain(); wxMain();
}) });

View File

@ -178,7 +178,7 @@ export class BrowserHttpLib implements HttpRequestLibrary {
): Promise<HttpResponse> { ): Promise<HttpResponse> {
return this.fetch(url, { return this.fetch(url, {
method: "POST", method: "POST",
headers: {"Content-Type": "application/json"}, headers: { "Content-Type": "application/json" },
body: JSON.stringify(body), body: JSON.stringify(body),
...opt, ...opt,
}); });

View File

@ -29,7 +29,9 @@ interface Type {
} }
const Context = createContext<Type>({ const Context = createContext<Type>({
devMode: false, devMode: false,
toggleDevMode: async () => { return; }, toggleDevMode: async () => {
return;
},
}); });
export const useDevContext = (): Type => useContext(Context); export const useDevContext = (): Type => useContext(Context);
@ -55,7 +57,8 @@ export const DevContextProviderForTesting = ({
export const DevContextProvider = ({ children }: { children: any }): VNode => { export const DevContextProvider = ({ children }: { children: any }): VNode => {
const [value, setter] = useLocalStorage("devMode"); const [value, setter] = useLocalStorage("devMode");
const devMode = value === "true"; const devMode = value === "true";
const toggleDevMode = async (): Promise<void> => setter((v) => (!v ? "true" : undefined)); const toggleDevMode = async (): Promise<void> =>
setter((v) => (!v ? "true" : undefined));
children = children =
children.length === 1 && typeof children === "function" children.length === 1 && typeof children === "function"
? children({ devMode }) ? children({ devMode })

View File

@ -40,10 +40,23 @@ const Context = createContext<Type>({
*/ */
export const useIocContext = (): Type => useContext(Context); export const useIocContext = (): Type => useContext(Context);
export const IoCProviderForTesting = ({ value, children }: { value: Type, children: any }): VNode => { export const IoCProviderForTesting = ({
value,
children,
}: {
value: Type;
children: any;
}): VNode => {
return h(Context.Provider, { value, children }); return h(Context.Provider, { value, children });
}; };
export const IoCProviderForRuntime = ({ children }: { children: any }): VNode => { export const IoCProviderForRuntime = ({
return h(Context.Provider, { value: { findTalerUriInActiveTab: platform.findTalerUriInActiveTab }, children }); children,
}: {
children: any;
}): VNode => {
return h(Context.Provider, {
value: { findTalerUriInActiveTab: platform.findTalerUriInActiveTab },
children,
});
}; };

View File

@ -27,7 +27,7 @@ import { strings } from "../i18n/strings.js";
interface Type { interface Type {
lang: string; lang: string;
supportedLang: { [id in keyof typeof supportedLang]: string } supportedLang: { [id in keyof typeof supportedLang]: string };
changeLanguage: (l: string) => void; changeLanguage: (l: string) => void;
i18n: typeof i18n; i18n: typeof i18n;
isSaved: boolean; isSaved: boolean;
@ -47,7 +47,6 @@ const supportedLang = {
navigator: "Defined by navigator", navigator: "Defined by navigator",
}; };
const initial = { const initial = {
lang: "en", lang: "en",
supportedLang, supportedLang,
@ -84,7 +83,10 @@ export const TranslationProvider = ({
} else { } else {
setupI18n(lang, strings); setupI18n(lang, strings);
} }
return h(Context.Provider, { value: { lang, changeLanguage, supportedLang, i18n, isSaved }, children }); return h(Context.Provider, {
value: { lang, changeLanguage, supportedLang, i18n, isSaved },
children,
});
}; };
export const useTranslationContext = (): Type => useContext(Context); export const useTranslationContext = (): Type => useContext(Context);

View File

@ -26,58 +26,61 @@ import { useComponentState } from "./Deposit.jsx";
describe("Deposit CTA states", () => { describe("Deposit CTA states", () => {
it("should tell the user that the URI is missing", async () => { it("should tell the user that the URI is missing", async () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() =>
useComponentState(undefined, undefined, { useComponentState(undefined, undefined, {
prepareRefund: async () => ({}), prepareRefund: async () => ({}),
applyRefund: async () => ({}), applyRefund: async () => ({}),
onUpdateNotification: async () => ({}) onUpdateNotification: async () => ({}),
} as any), } as any),
); );
{ {
const { status, hook } = getLastResultOrThrow() const { status, hook } = getLastResultOrThrow();
expect(status).equals('loading') expect(status).equals("loading");
expect(hook).undefined; expect(hook).undefined;
} }
await waitNextUpdate() await waitNextUpdate();
{ {
const { status, hook } = getLastResultOrThrow() const { status, hook } = getLastResultOrThrow();
expect(status).equals('loading') expect(status).equals("loading");
if (!hook) expect.fail(); if (!hook) expect.fail();
if (!hook.hasError) expect.fail(); if (!hook.hasError) expect.fail();
if (hook.operational) expect.fail(); if (hook.operational) expect.fail();
expect(hook.message).eq("ERROR_NO-URI-FOR-DEPOSIT"); expect(hook.message).eq("ERROR_NO-URI-FOR-DEPOSIT");
} }
await assertNoPendingUpdate() await assertNoPendingUpdate();
}); });
it("should be ready after loading", async () => { it("should be ready after loading", async () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() =>
useComponentState("payto://refund/asdasdas", "EUR:1", { useComponentState("payto://refund/asdasdas", "EUR:1", {
prepareDeposit: async () => ({ prepareDeposit: async () =>
({
effectiveDepositAmount: Amounts.parseOrThrow("EUR:1"), effectiveDepositAmount: Amounts.parseOrThrow("EUR:1"),
totalDepositCost: Amounts.parseOrThrow("EUR:1.2") totalDepositCost: Amounts.parseOrThrow("EUR:1.2"),
} as PrepareDepositResponse as any), } as PrepareDepositResponse as any),
createDepositGroup: async () => ({}), createDepositGroup: async () => ({}),
} as any), } as any),
); );
{ {
const { status, hook } = getLastResultOrThrow() const { status, hook } = getLastResultOrThrow();
expect(status).equals('loading') expect(status).equals("loading");
expect(hook).undefined; expect(hook).undefined;
} }
await waitNextUpdate() await waitNextUpdate();
{ {
const state = getLastResultOrThrow() const state = getLastResultOrThrow();
if (state.status !== 'ready') expect.fail(); if (state.status !== "ready") expect.fail();
if (state.hook) expect.fail(); if (state.hook) expect.fail();
expect(state.confirm.onClick).not.undefined; expect(state.confirm.onClick).not.undefined;
expect(state.cost).deep.eq(Amounts.parseOrThrow("EUR:1.2")); expect(state.cost).deep.eq(Amounts.parseOrThrow("EUR:1.2"));
@ -85,8 +88,6 @@ describe("Deposit CTA states", () => {
expect(state.effective).deep.eq(Amounts.parseOrThrow("EUR:1")); expect(state.effective).deep.eq(Amounts.parseOrThrow("EUR:1"));
} }
await assertNoPendingUpdate() await assertNoPendingUpdate();
}); });
}); });

View File

@ -19,7 +19,16 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { AmountJson, Amounts, BalancesResponse, ConfirmPayResult, ConfirmPayResultType, NotificationType, PreparePayResult, PreparePayResultType } from "@gnu-taler/taler-util"; import {
AmountJson,
Amounts,
BalancesResponse,
ConfirmPayResult,
ConfirmPayResultType,
NotificationType,
PreparePayResult,
PreparePayResultType,
} from "@gnu-taler/taler-util";
import { expect } from "chai"; import { expect } from "chai";
import { mountHook } from "../test-utils.js"; import { mountHook } from "../test-utils.js";
import * as wxApi from "../wxApi.js"; import * as wxApi from "../wxApi.js";
@ -29,8 +38,8 @@ const nullFunction: any = () => null;
type VoidFunction = () => void; type VoidFunction = () => void;
type Subs = { type Subs = {
[key in NotificationType]?: VoidFunction [key in NotificationType]?: VoidFunction;
} };
export class SubsHandler { export class SubsHandler {
private subs: Subs = {}; private subs: Subs = {};
@ -39,311 +48,340 @@ export class SubsHandler {
this.saveSubscription = this.saveSubscription.bind(this); this.saveSubscription = this.saveSubscription.bind(this);
} }
saveSubscription(messageTypes: NotificationType[], callback: VoidFunction): VoidFunction { saveSubscription(
messageTypes.forEach(m => { messageTypes: NotificationType[],
callback: VoidFunction,
): VoidFunction {
messageTypes.forEach((m) => {
this.subs[m] = callback; this.subs[m] = callback;
}) });
return nullFunction; return nullFunction;
} }
notifyEvent(event: NotificationType): void { notifyEvent(event: NotificationType): void {
const cb = this.subs[event]; const cb = this.subs[event];
if (cb === undefined) expect.fail(`Expected to have a subscription for ${event}`); if (cb === undefined)
cb() expect.fail(`Expected to have a subscription for ${event}`);
cb();
} }
} }
describe("Pay CTA states", () => { describe("Pay CTA states", () => {
it("should tell the user that the URI is missing", async () => { it("should tell the user that the URI is missing", async () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => mountHook(() =>
useComponentState(undefined, { useComponentState(undefined, {
onUpdateNotification: nullFunction, onUpdateNotification: nullFunction,
} as Partial<typeof wxApi> as any) } as Partial<typeof wxApi> as any),
); );
{ {
const { status, hook } = getLastResultOrThrow() const { status, hook } = getLastResultOrThrow();
expect(status).equals('loading') expect(status).equals("loading");
expect(hook).undefined; expect(hook).undefined;
} }
await waitNextUpdate() await waitNextUpdate();
{ {
const { status, hook } = getLastResultOrThrow() const { status, hook } = getLastResultOrThrow();
expect(status).equals('loading') expect(status).equals("loading");
if (hook === undefined) expect.fail() if (hook === undefined) expect.fail();
expect(hook.hasError).true; expect(hook.hasError).true;
expect(hook.operational).false; expect(hook.operational).false;
} }
await assertNoPendingUpdate() await assertNoPendingUpdate();
}); });
it("should response with no balance", async () => { it("should response with no balance", async () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => mountHook(() =>
useComponentState('taller://pay', { useComponentState("taller://pay", {
onUpdateNotification: nullFunction, onUpdateNotification: nullFunction,
preparePay: async () => ({ preparePay: async () =>
amountRaw: 'USD:10', ({
amountRaw: "USD:10",
status: PreparePayResultType.InsufficientBalance, status: PreparePayResultType.InsufficientBalance,
} as Partial<PreparePayResult>), } as Partial<PreparePayResult>),
getBalance: async () => ({ getBalance: async () =>
balances: [] ({
balances: [],
} as Partial<BalancesResponse>), } as Partial<BalancesResponse>),
} as Partial<typeof wxApi> as any) } as Partial<typeof wxApi> as any),
); );
{ {
const { status, hook } = getLastResultOrThrow() const { status, hook } = getLastResultOrThrow();
expect(status).equals('loading') expect(status).equals("loading");
expect(hook).undefined; expect(hook).undefined;
} }
await waitNextUpdate(); await waitNextUpdate();
{ {
const r = getLastResultOrThrow() const r = getLastResultOrThrow();
if (r.status !== 'ready') expect.fail() if (r.status !== "ready") expect.fail();
expect(r.balance).undefined; expect(r.balance).undefined;
expect(r.amount).deep.equal(Amounts.parseOrThrow('USD:10')) expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:10"));
expect(r.payHandler.onClick).undefined; expect(r.payHandler.onClick).undefined;
} }
await assertNoPendingUpdate() await assertNoPendingUpdate();
}); });
it("should not be able to pay if there is no enough balance", async () => { it("should not be able to pay if there is no enough balance", async () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => mountHook(() =>
useComponentState('taller://pay', { useComponentState("taller://pay", {
onUpdateNotification: nullFunction, onUpdateNotification: nullFunction,
preparePay: async () => ({ preparePay: async () =>
amountRaw: 'USD:10', ({
amountRaw: "USD:10",
status: PreparePayResultType.InsufficientBalance, status: PreparePayResultType.InsufficientBalance,
} as Partial<PreparePayResult>), } as Partial<PreparePayResult>),
getBalance: async () => ({ getBalance: async () =>
balances: [{ ({
available: 'USD:5' balances: [
}] {
available: "USD:5",
},
],
} as Partial<BalancesResponse>), } as Partial<BalancesResponse>),
} as Partial<typeof wxApi> as any) } as Partial<typeof wxApi> as any),
); );
{ {
const { status, hook } = getLastResultOrThrow() const { status, hook } = getLastResultOrThrow();
expect(status).equals('loading') expect(status).equals("loading");
expect(hook).undefined; expect(hook).undefined;
} }
await waitNextUpdate(); await waitNextUpdate();
{ {
const r = getLastResultOrThrow() const r = getLastResultOrThrow();
if (r.status !== 'ready') expect.fail() if (r.status !== "ready") expect.fail();
expect(r.balance).deep.equal(Amounts.parseOrThrow('USD:5')); expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:5"));
expect(r.amount).deep.equal(Amounts.parseOrThrow('USD:10')) expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:10"));
expect(r.payHandler.onClick).undefined; expect(r.payHandler.onClick).undefined;
} }
await assertNoPendingUpdate() await assertNoPendingUpdate();
}); });
it("should be able to pay (without fee)", async () => { it("should be able to pay (without fee)", async () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => mountHook(() =>
useComponentState('taller://pay', { useComponentState("taller://pay", {
onUpdateNotification: nullFunction, onUpdateNotification: nullFunction,
preparePay: async () => ({ preparePay: async () =>
amountRaw: 'USD:10', ({
amountEffective: 'USD:10', amountRaw: "USD:10",
amountEffective: "USD:10",
status: PreparePayResultType.PaymentPossible, status: PreparePayResultType.PaymentPossible,
} as Partial<PreparePayResult>), } as Partial<PreparePayResult>),
getBalance: async () => ({ getBalance: async () =>
balances: [{ ({
available: 'USD:15' balances: [
}] {
available: "USD:15",
},
],
} as Partial<BalancesResponse>), } as Partial<BalancesResponse>),
} as Partial<typeof wxApi> as any) } as Partial<typeof wxApi> as any),
); );
{ {
const { status, hook } = getLastResultOrThrow() const { status, hook } = getLastResultOrThrow();
expect(status).equals('loading') expect(status).equals("loading");
expect(hook).undefined; expect(hook).undefined;
} }
await waitNextUpdate(); await waitNextUpdate();
{ {
const r = getLastResultOrThrow() const r = getLastResultOrThrow();
if (r.status !== 'ready') expect.fail() if (r.status !== "ready") expect.fail();
expect(r.balance).deep.equal(Amounts.parseOrThrow('USD:15')); expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
expect(r.amount).deep.equal(Amounts.parseOrThrow('USD:10')) expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:10"));
expect(r.totalFees).deep.equal(Amounts.parseOrThrow('USD:0')) expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:0"));
expect(r.payHandler.onClick).not.undefined; expect(r.payHandler.onClick).not.undefined;
} }
await assertNoPendingUpdate() await assertNoPendingUpdate();
}); });
it("should be able to pay (with fee)", async () => { it("should be able to pay (with fee)", async () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => mountHook(() =>
useComponentState('taller://pay', { useComponentState("taller://pay", {
onUpdateNotification: nullFunction, onUpdateNotification: nullFunction,
preparePay: async () => ({ preparePay: async () =>
amountRaw: 'USD:9', ({
amountEffective: 'USD:10', amountRaw: "USD:9",
amountEffective: "USD:10",
status: PreparePayResultType.PaymentPossible, status: PreparePayResultType.PaymentPossible,
} as Partial<PreparePayResult>), } as Partial<PreparePayResult>),
getBalance: async () => ({ getBalance: async () =>
balances: [{ ({
available: 'USD:15' balances: [
}] {
available: "USD:15",
},
],
} as Partial<BalancesResponse>), } as Partial<BalancesResponse>),
} as Partial<typeof wxApi> as any) } as Partial<typeof wxApi> as any),
); );
{ {
const { status, hook } = getLastResultOrThrow() const { status, hook } = getLastResultOrThrow();
expect(status).equals('loading') expect(status).equals("loading");
expect(hook).undefined; expect(hook).undefined;
} }
await waitNextUpdate(); await waitNextUpdate();
{ {
const r = getLastResultOrThrow() const r = getLastResultOrThrow();
if (r.status !== 'ready') expect.fail() if (r.status !== "ready") expect.fail();
expect(r.balance).deep.equal(Amounts.parseOrThrow('USD:15')); expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
expect(r.amount).deep.equal(Amounts.parseOrThrow('USD:9')) expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
expect(r.totalFees).deep.equal(Amounts.parseOrThrow('USD:1')) expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
expect(r.payHandler.onClick).not.undefined; expect(r.payHandler.onClick).not.undefined;
} }
await assertNoPendingUpdate() await assertNoPendingUpdate();
}); });
it("should get confirmation done after pay successfully", async () => { it("should get confirmation done after pay successfully", async () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => mountHook(() =>
useComponentState('taller://pay', { useComponentState("taller://pay", {
onUpdateNotification: nullFunction, onUpdateNotification: nullFunction,
preparePay: async () => ({ preparePay: async () =>
amountRaw: 'USD:9', ({
amountEffective: 'USD:10', amountRaw: "USD:9",
amountEffective: "USD:10",
status: PreparePayResultType.PaymentPossible, status: PreparePayResultType.PaymentPossible,
} as Partial<PreparePayResult>), } as Partial<PreparePayResult>),
getBalance: async () => ({ getBalance: async () =>
balances: [{ ({
available: 'USD:15' balances: [
}] {
available: "USD:15",
},
],
} as Partial<BalancesResponse>), } as Partial<BalancesResponse>),
confirmPay: async () => ({ confirmPay: async () =>
({
type: ConfirmPayResultType.Done, type: ConfirmPayResultType.Done,
contractTerms: {} contractTerms: {},
} as Partial<ConfirmPayResult>), } as Partial<ConfirmPayResult>),
} as Partial<typeof wxApi> as any) } as Partial<typeof wxApi> as any),
); );
{ {
const { status, hook } = getLastResultOrThrow() const { status, hook } = getLastResultOrThrow();
expect(status).equals('loading') expect(status).equals("loading");
expect(hook).undefined; expect(hook).undefined;
} }
await waitNextUpdate(); await waitNextUpdate();
{ {
const r = getLastResultOrThrow() const r = getLastResultOrThrow();
if (r.status !== 'ready') expect.fail() if (r.status !== "ready") expect.fail();
expect(r.balance).deep.equal(Amounts.parseOrThrow('USD:15')); expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
expect(r.amount).deep.equal(Amounts.parseOrThrow('USD:9')) expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
expect(r.totalFees).deep.equal(Amounts.parseOrThrow('USD:1')) expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
if (r.payHandler.onClick === undefined) expect.fail(); if (r.payHandler.onClick === undefined) expect.fail();
r.payHandler.onClick() r.payHandler.onClick();
} }
await waitNextUpdate(); await waitNextUpdate();
{ {
const r = getLastResultOrThrow() const r = getLastResultOrThrow();
if (r.status !== 'confirmed') expect.fail() if (r.status !== "confirmed") expect.fail();
expect(r.balance).deep.equal(Amounts.parseOrThrow('USD:15')); expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
expect(r.amount).deep.equal(Amounts.parseOrThrow('USD:9')) expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
expect(r.totalFees).deep.equal(Amounts.parseOrThrow('USD:1')) expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
if (r.payResult.type !== ConfirmPayResultType.Done) expect.fail(); if (r.payResult.type !== ConfirmPayResultType.Done) expect.fail();
expect(r.payResult.contractTerms).not.undefined; expect(r.payResult.contractTerms).not.undefined;
expect(r.payHandler.onClick).undefined; expect(r.payHandler.onClick).undefined;
} }
await assertNoPendingUpdate() await assertNoPendingUpdate();
}); });
it("should not stay in ready state after pay with error", async () => { it("should not stay in ready state after pay with error", async () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => mountHook(() =>
useComponentState('taller://pay', { useComponentState("taller://pay", {
onUpdateNotification: nullFunction, onUpdateNotification: nullFunction,
preparePay: async () => ({ preparePay: async () =>
amountRaw: 'USD:9', ({
amountEffective: 'USD:10', amountRaw: "USD:9",
amountEffective: "USD:10",
status: PreparePayResultType.PaymentPossible, status: PreparePayResultType.PaymentPossible,
} as Partial<PreparePayResult>), } as Partial<PreparePayResult>),
getBalance: async () => ({ getBalance: async () =>
balances: [{ ({
available: 'USD:15' balances: [
}] {
available: "USD:15",
},
],
} as Partial<BalancesResponse>), } as Partial<BalancesResponse>),
confirmPay: async () => ({ confirmPay: async () =>
({
type: ConfirmPayResultType.Pending, type: ConfirmPayResultType.Pending,
lastError: { code: 1 }, lastError: { code: 1 },
} as Partial<ConfirmPayResult>), } as Partial<ConfirmPayResult>),
} as Partial<typeof wxApi> as any) } as Partial<typeof wxApi> as any),
); );
{ {
const { status, hook } = getLastResultOrThrow() const { status, hook } = getLastResultOrThrow();
expect(status).equals('loading') expect(status).equals("loading");
expect(hook).undefined; expect(hook).undefined;
} }
await waitNextUpdate(); await waitNextUpdate();
{ {
const r = getLastResultOrThrow() const r = getLastResultOrThrow();
if (r.status !== 'ready') expect.fail() if (r.status !== "ready") expect.fail();
expect(r.balance).deep.equal(Amounts.parseOrThrow('USD:15')); expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
expect(r.amount).deep.equal(Amounts.parseOrThrow('USD:9')) expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
expect(r.totalFees).deep.equal(Amounts.parseOrThrow('USD:1')) expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
if (r.payHandler.onClick === undefined) expect.fail(); if (r.payHandler.onClick === undefined) expect.fail();
r.payHandler.onClick() r.payHandler.onClick();
} }
await waitNextUpdate(); await waitNextUpdate();
{ {
const r = getLastResultOrThrow() const r = getLastResultOrThrow();
if (r.status !== 'ready') expect.fail() if (r.status !== "ready") expect.fail();
expect(r.balance).deep.equal(Amounts.parseOrThrow('USD:15')); expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
expect(r.amount).deep.equal(Amounts.parseOrThrow('USD:9')) expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
expect(r.totalFees).deep.equal(Amounts.parseOrThrow('USD:1')) expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
expect(r.payHandler.onClick).undefined; expect(r.payHandler.onClick).undefined;
if (r.payHandler.error === undefined) expect.fail(); if (r.payHandler.error === undefined) expect.fail();
//FIXME: error message here is bad //FIXME: error message here is bad
expect(r.payHandler.error.errorDetail.hint).eq("could not confirm payment") expect(r.payHandler.error.errorDetail.hint).eq(
"could not confirm payment",
);
expect(r.payHandler.error.errorDetail.payResult).deep.equal({ expect(r.payHandler.error.errorDetail.payResult).deep.equal({
type: ConfirmPayResultType.Pending, type: ConfirmPayResultType.Pending,
lastError: { code: 1 } lastError: { code: 1 },
}) });
} }
await assertNoPendingUpdate() await assertNoPendingUpdate();
}); });
it("should update balance if a coins is withdraw", async () => { it("should update balance if a coins is withdraw", async () => {
@ -351,40 +389,45 @@ describe("Pay CTA states", () => {
let availableBalance = Amounts.parseOrThrow("USD:10"); let availableBalance = Amounts.parseOrThrow("USD:10");
function notifyCoinWithdrawn(newAmount: AmountJson): void { function notifyCoinWithdrawn(newAmount: AmountJson): void {
availableBalance = Amounts.add(availableBalance, newAmount).amount availableBalance = Amounts.add(availableBalance, newAmount).amount;
subscriptions.notifyEvent(NotificationType.CoinWithdrawn) subscriptions.notifyEvent(NotificationType.CoinWithdrawn);
} }
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
useComponentState('taller://pay', { mountHook(() =>
useComponentState("taller://pay", {
onUpdateNotification: subscriptions.saveSubscription, onUpdateNotification: subscriptions.saveSubscription,
preparePay: async () => ({ preparePay: async () =>
amountRaw: 'USD:9', ({
amountEffective: 'USD:10', amountRaw: "USD:9",
amountEffective: "USD:10",
status: PreparePayResultType.PaymentPossible, status: PreparePayResultType.PaymentPossible,
} as Partial<PreparePayResult>), } as Partial<PreparePayResult>),
getBalance: async () => ({ getBalance: async () =>
balances: [{ ({
available: Amounts.stringify(availableBalance) balances: [
}] {
available: Amounts.stringify(availableBalance),
},
],
} as Partial<BalancesResponse>), } as Partial<BalancesResponse>),
} as Partial<typeof wxApi> as any) } as Partial<typeof wxApi> as any),
); );
{ {
const { status, hook } = getLastResultOrThrow() const { status, hook } = getLastResultOrThrow();
expect(status).equals('loading') expect(status).equals("loading");
expect(hook).undefined; expect(hook).undefined;
} }
await waitNextUpdate(); await waitNextUpdate();
{ {
const r = getLastResultOrThrow() const r = getLastResultOrThrow();
if (r.status !== 'ready') expect.fail() if (r.status !== "ready") expect.fail();
expect(r.balance).deep.equal(Amounts.parseOrThrow('USD:10')); expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:10"));
expect(r.amount).deep.equal(Amounts.parseOrThrow('USD:9')) expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
expect(r.totalFees).deep.equal(Amounts.parseOrThrow('USD:1')) expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
expect(r.payHandler.onClick).not.undefined; expect(r.payHandler.onClick).not.undefined;
notifyCoinWithdrawn(Amounts.parseOrThrow("USD:5")); notifyCoinWithdrawn(Amounts.parseOrThrow("USD:5"));
@ -393,16 +436,14 @@ describe("Pay CTA states", () => {
await waitNextUpdate(); await waitNextUpdate();
{ {
const r = getLastResultOrThrow() const r = getLastResultOrThrow();
if (r.status !== 'ready') expect.fail() if (r.status !== "ready") expect.fail();
expect(r.balance).deep.equal(Amounts.parseOrThrow('USD:15')); expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:15"));
expect(r.amount).deep.equal(Amounts.parseOrThrow('USD:9')) expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:9"));
expect(r.totalFees).deep.equal(Amounts.parseOrThrow('USD:1')) expect(r.totalFees).deep.equal(Amounts.parseOrThrow("USD:1"));
expect(r.payHandler.onClick).not.undefined; expect(r.payHandler.onClick).not.undefined;
} }
await assertNoPendingUpdate() await assertNoPendingUpdate();
}); });
}); });

View File

@ -19,7 +19,12 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { AmountJson, Amounts, NotificationType, PrepareRefundResult } from "@gnu-taler/taler-util"; import {
AmountJson,
Amounts,
NotificationType,
PrepareRefundResult,
} from "@gnu-taler/taler-util";
import { expect } from "chai"; import { expect } from "chai";
import { mountHook } from "../test-utils.js"; import { mountHook } from "../test-utils.js";
import { SubsHandler } from "./Pay.test.js"; import { SubsHandler } from "./Pay.test.js";
@ -29,146 +34,151 @@ import { useComponentState } from "./Refund.jsx";
describe("Refund CTA states", () => { describe("Refund CTA states", () => {
it("should tell the user that the URI is missing", async () => { it("should tell the user that the URI is missing", async () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() =>
useComponentState(undefined, { useComponentState(undefined, {
prepareRefund: async () => ({}), prepareRefund: async () => ({}),
applyRefund: async () => ({}), applyRefund: async () => ({}),
onUpdateNotification: async () => ({}) onUpdateNotification: async () => ({}),
} as any), } as any),
); );
{ {
const { status, hook } = getLastResultOrThrow() const { status, hook } = getLastResultOrThrow();
expect(status).equals('loading') expect(status).equals("loading");
expect(hook).undefined; expect(hook).undefined;
} }
await waitNextUpdate() await waitNextUpdate();
{ {
const { status, hook } = getLastResultOrThrow() const { status, hook } = getLastResultOrThrow();
expect(status).equals('loading') expect(status).equals("loading");
if (!hook) expect.fail(); if (!hook) expect.fail();
if (!hook.hasError) expect.fail(); if (!hook.hasError) expect.fail();
if (hook.operational) expect.fail(); if (hook.operational) expect.fail();
expect(hook.message).eq("ERROR_NO-URI-FOR-REFUND"); expect(hook.message).eq("ERROR_NO-URI-FOR-REFUND");
} }
await assertNoPendingUpdate() await assertNoPendingUpdate();
}); });
it("should be ready after loading", async () => { it("should be ready after loading", async () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() =>
useComponentState("taler://refund/asdasdas", { useComponentState("taler://refund/asdasdas", {
prepareRefund: async () => ({ prepareRefund: async () =>
effectivePaid: 'EUR:2', ({
awaiting: 'EUR:2', effectivePaid: "EUR:2",
gone: 'EUR:0', awaiting: "EUR:2",
granted: 'EUR:0', gone: "EUR:0",
granted: "EUR:0",
pending: false, pending: false,
proposalId: '1', proposalId: "1",
info: { info: {
contractTermsHash: '123', contractTermsHash: "123",
merchant: { merchant: {
name: 'the merchant name' name: "the merchant name",
},
orderId: "orderId1",
summary: "the sumary",
}, },
orderId: 'orderId1',
summary: 'the sumary'
}
} as PrepareRefundResult as any), } as PrepareRefundResult as any),
applyRefund: async () => ({}), applyRefund: async () => ({}),
onUpdateNotification: async () => ({}) onUpdateNotification: async () => ({}),
} as any), } as any),
); );
{ {
const { status, hook } = getLastResultOrThrow() const { status, hook } = getLastResultOrThrow();
expect(status).equals('loading') expect(status).equals("loading");
expect(hook).undefined; expect(hook).undefined;
} }
await waitNextUpdate() await waitNextUpdate();
{ {
const state = getLastResultOrThrow() const state = getLastResultOrThrow();
if (state.status !== 'ready') expect.fail(); if (state.status !== "ready") expect.fail();
if (state.hook) expect.fail(); if (state.hook) expect.fail();
expect(state.accept.onClick).not.undefined; expect(state.accept.onClick).not.undefined;
expect(state.ignore.onClick).not.undefined; expect(state.ignore.onClick).not.undefined;
expect(state.merchantName).eq('the merchant name'); expect(state.merchantName).eq("the merchant name");
expect(state.orderId).eq('orderId1'); expect(state.orderId).eq("orderId1");
expect(state.products).undefined; expect(state.products).undefined;
} }
await assertNoPendingUpdate() await assertNoPendingUpdate();
}); });
it("should be ignored after clicking the ignore button", async () => { it("should be ignored after clicking the ignore button", async () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() =>
useComponentState("taler://refund/asdasdas", { useComponentState("taler://refund/asdasdas", {
prepareRefund: async () => ({ prepareRefund: async () =>
effectivePaid: 'EUR:2', ({
awaiting: 'EUR:2', effectivePaid: "EUR:2",
gone: 'EUR:0', awaiting: "EUR:2",
granted: 'EUR:0', gone: "EUR:0",
granted: "EUR:0",
pending: false, pending: false,
proposalId: '1', proposalId: "1",
info: { info: {
contractTermsHash: '123', contractTermsHash: "123",
merchant: { merchant: {
name: 'the merchant name' name: "the merchant name",
},
orderId: "orderId1",
summary: "the sumary",
}, },
orderId: 'orderId1',
summary: 'the sumary'
}
} as PrepareRefundResult as any), } as PrepareRefundResult as any),
applyRefund: async () => ({}), applyRefund: async () => ({}),
onUpdateNotification: async () => ({}) onUpdateNotification: async () => ({}),
} as any), } as any),
); );
{ {
const { status, hook } = getLastResultOrThrow() const { status, hook } = getLastResultOrThrow();
expect(status).equals('loading') expect(status).equals("loading");
expect(hook).undefined; expect(hook).undefined;
} }
await waitNextUpdate() await waitNextUpdate();
{ {
const state = getLastResultOrThrow() const state = getLastResultOrThrow();
if (state.status !== 'ready') expect.fail(); if (state.status !== "ready") expect.fail();
if (state.hook) expect.fail(); if (state.hook) expect.fail();
expect(state.accept.onClick).not.undefined; expect(state.accept.onClick).not.undefined;
expect(state.merchantName).eq('the merchant name'); expect(state.merchantName).eq("the merchant name");
expect(state.orderId).eq('orderId1'); expect(state.orderId).eq("orderId1");
expect(state.products).undefined; expect(state.products).undefined;
if (state.ignore.onClick === undefined) expect.fail(); if (state.ignore.onClick === undefined) expect.fail();
state.ignore.onClick() state.ignore.onClick();
} }
await waitNextUpdate() await waitNextUpdate();
{ {
const state = getLastResultOrThrow() const state = getLastResultOrThrow();
if (state.status !== 'ignored') expect.fail(); if (state.status !== "ignored") expect.fail();
if (state.hook) expect.fail(); if (state.hook) expect.fail();
expect(state.merchantName).eq('the merchant name'); expect(state.merchantName).eq("the merchant name");
} }
await assertNoPendingUpdate() await assertNoPendingUpdate();
}); });
it("should be in progress when doing refresh", async () => { it("should be in progress when doing refresh", async () => {
let granted = Amounts.getZero('EUR') let granted = Amounts.getZero("EUR");
const unit: AmountJson = { currency: 'EUR', value: 1, fraction: 0 } const unit: AmountJson = { currency: "EUR", value: 1, fraction: 0 };
const refunded: AmountJson = { currency: 'EUR', value: 2, fraction: 0 } const refunded: AmountJson = { currency: "EUR", value: 2, fraction: 0 };
let awaiting: AmountJson = refunded let awaiting: AmountJson = refunded;
let pending = true; let pending = true;
const subscriptions = new SubsHandler(); const subscriptions = new SubsHandler();
@ -177,26 +187,28 @@ describe("Refund CTA states", () => {
granted = Amounts.add(granted, unit).amount; granted = Amounts.add(granted, unit).amount;
pending = granted.value < refunded.value; pending = granted.value < refunded.value;
awaiting = Amounts.sub(refunded, granted).amount; awaiting = Amounts.sub(refunded, granted).amount;
subscriptions.notifyEvent(NotificationType.RefreshMelted) subscriptions.notifyEvent(NotificationType.RefreshMelted);
} }
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() =>
useComponentState("taler://refund/asdasdas", { useComponentState("taler://refund/asdasdas", {
prepareRefund: async () => ({ prepareRefund: async () =>
({
awaiting: Amounts.stringify(awaiting), awaiting: Amounts.stringify(awaiting),
effectivePaid: 'EUR:2', effectivePaid: "EUR:2",
gone: 'EUR:0', gone: "EUR:0",
granted: Amounts.stringify(granted), granted: Amounts.stringify(granted),
pending, pending,
proposalId: '1', proposalId: "1",
info: { info: {
contractTermsHash: '123', contractTermsHash: "123",
merchant: { merchant: {
name: 'the merchant name' name: "the merchant name",
},
orderId: "orderId1",
summary: "the sumary",
}, },
orderId: 'orderId1',
summary: 'the sumary'
}
} as PrepareRefundResult as any), } as PrepareRefundResult as any),
applyRefund: async () => ({}), applyRefund: async () => ({}),
onUpdateNotification: subscriptions.saveSubscription, onUpdateNotification: subscriptions.saveSubscription,
@ -204,53 +216,53 @@ describe("Refund CTA states", () => {
); );
{ {
const { status, hook } = getLastResultOrThrow() const { status, hook } = getLastResultOrThrow();
expect(status).equals('loading') expect(status).equals("loading");
expect(hook).undefined; expect(hook).undefined;
} }
await waitNextUpdate() await waitNextUpdate();
{ {
const state = getLastResultOrThrow() const state = getLastResultOrThrow();
if (state.status !== 'in-progress') expect.fail('1'); if (state.status !== "in-progress") expect.fail("1");
if (state.hook) expect.fail(); if (state.hook) expect.fail();
expect(state.merchantName).eq('the merchant name'); expect(state.merchantName).eq("the merchant name");
expect(state.products).undefined; expect(state.products).undefined;
expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:2")) expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:2"));
// expect(state.progress).closeTo(1 / 3, 0.01) // expect(state.progress).closeTo(1 / 3, 0.01)
notifyMelt() notifyMelt();
} }
await waitNextUpdate() await waitNextUpdate();
{ {
const state = getLastResultOrThrow() const state = getLastResultOrThrow();
if (state.status !== 'in-progress') expect.fail('2'); if (state.status !== "in-progress") expect.fail("2");
if (state.hook) expect.fail(); if (state.hook) expect.fail();
expect(state.merchantName).eq('the merchant name'); expect(state.merchantName).eq("the merchant name");
expect(state.products).undefined; expect(state.products).undefined;
expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:2")) expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:2"));
// expect(state.progress).closeTo(2 / 3, 0.01) // expect(state.progress).closeTo(2 / 3, 0.01)
notifyMelt() notifyMelt();
} }
await waitNextUpdate() await waitNextUpdate();
{ {
const state = getLastResultOrThrow() const state = getLastResultOrThrow();
if (state.status !== 'completed') expect.fail('3'); if (state.status !== "completed") expect.fail("3");
if (state.hook) expect.fail(); if (state.hook) expect.fail();
expect(state.merchantName).eq('the merchant name'); expect(state.merchantName).eq("the merchant name");
expect(state.products).undefined; expect(state.products).undefined;
expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:2")) expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:2"));
} }
await assertNoPendingUpdate() await assertNoPendingUpdate();
}); });
}); });

View File

@ -26,40 +26,43 @@ import { useComponentState } from "./Tip.jsx";
describe("Tip CTA states", () => { describe("Tip CTA states", () => {
it("should tell the user that the URI is missing", async () => { it("should tell the user that the URI is missing", async () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() =>
useComponentState(undefined, { useComponentState(undefined, {
prepareTip: async () => ({}), prepareTip: async () => ({}),
acceptTip: async () => ({}) acceptTip: async () => ({}),
} as any), } as any),
); );
{ {
const { status, hook } = getLastResultOrThrow() const { status, hook } = getLastResultOrThrow();
expect(status).equals('loading') expect(status).equals("loading");
expect(hook).undefined; expect(hook).undefined;
} }
await waitNextUpdate() await waitNextUpdate();
{ {
const { status, hook } = getLastResultOrThrow() const { status, hook } = getLastResultOrThrow();
expect(status).equals('loading') expect(status).equals("loading");
if (!hook) expect.fail(); if (!hook) expect.fail();
if (!hook.hasError) expect.fail(); if (!hook.hasError) expect.fail();
if (hook.operational) expect.fail(); if (hook.operational) expect.fail();
expect(hook.message).eq("ERROR_NO-URI-FOR-TIP"); expect(hook.message).eq("ERROR_NO-URI-FOR-TIP");
} }
await assertNoPendingUpdate() await assertNoPendingUpdate();
}); });
it("should be ready for accepting the tip", async () => { it("should be ready for accepting the tip", async () => {
let tipAccepted = false; let tipAccepted = false;
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() =>
useComponentState("taler://tip/asd", { useComponentState("taler://tip/asd", {
prepareTip: async () => ({ prepareTip: async () =>
({
accepted: tipAccepted, accepted: tipAccepted,
exchangeBaseUrl: "exchange url", exchangeBaseUrl: "exchange url",
merchantBaseUrl: "merchant url", merchantBaseUrl: "merchant url",
@ -67,23 +70,23 @@ describe("Tip CTA states", () => {
walletTipId: "tip_id", walletTipId: "tip_id",
} as PrepareTipResult as any), } as PrepareTipResult as any),
acceptTip: async () => { acceptTip: async () => {
tipAccepted = true tipAccepted = true;
} },
} as any), } as any),
); );
{ {
const { status, hook } = getLastResultOrThrow() const { status, hook } = getLastResultOrThrow();
expect(status).equals('loading') expect(status).equals("loading");
expect(hook).undefined; expect(hook).undefined;
} }
await waitNextUpdate() await waitNextUpdate();
{ {
const state = getLastResultOrThrow() const state = getLastResultOrThrow();
if (state.status !== "ready") expect.fail() if (state.status !== "ready") expect.fail();
if (state.hook) expect.fail(); if (state.hook) expect.fail();
expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:1")); expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:1"));
expect(state.merchantBaseUrl).eq("merchant url"); expect(state.merchantBaseUrl).eq("merchant url");
@ -93,45 +96,46 @@ describe("Tip CTA states", () => {
state.accept.onClick(); state.accept.onClick();
} }
await waitNextUpdate() await waitNextUpdate();
{ {
const state = getLastResultOrThrow() const state = getLastResultOrThrow();
if (state.status !== "accepted") expect.fail() if (state.status !== "accepted") expect.fail();
if (state.hook) expect.fail(); if (state.hook) expect.fail();
expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:1")); expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:1"));
expect(state.merchantBaseUrl).eq("merchant url"); expect(state.merchantBaseUrl).eq("merchant url");
expect(state.exchangeBaseUrl).eq("exchange url"); expect(state.exchangeBaseUrl).eq("exchange url");
} }
await assertNoPendingUpdate() await assertNoPendingUpdate();
}); });
it("should be ignored after clicking the ignore button", async () => { it("should be ignored after clicking the ignore button", async () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() =>
useComponentState("taler://tip/asd", { useComponentState("taler://tip/asd", {
prepareTip: async () => ({ prepareTip: async () =>
({
exchangeBaseUrl: "exchange url", exchangeBaseUrl: "exchange url",
merchantBaseUrl: "merchant url", merchantBaseUrl: "merchant url",
tipAmountEffective: "EUR:1", tipAmountEffective: "EUR:1",
walletTipId: "tip_id", walletTipId: "tip_id",
} as PrepareTipResult as any), } as PrepareTipResult as any),
acceptTip: async () => ({}) acceptTip: async () => ({}),
} as any), } as any),
); );
{ {
const { status, hook } = getLastResultOrThrow() const { status, hook } = getLastResultOrThrow();
expect(status).equals('loading') expect(status).equals("loading");
expect(hook).undefined; expect(hook).undefined;
} }
await waitNextUpdate() await waitNextUpdate();
{ {
const state = getLastResultOrThrow() const state = getLastResultOrThrow();
if (state.status !== "ready") expect.fail() if (state.status !== "ready") expect.fail();
if (state.hook) expect.fail(); if (state.hook) expect.fail();
expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:1")); expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:1"));
expect(state.merchantBaseUrl).eq("merchant url"); expect(state.merchantBaseUrl).eq("merchant url");
@ -141,52 +145,49 @@ describe("Tip CTA states", () => {
state.ignore.onClick(); state.ignore.onClick();
} }
await waitNextUpdate() await waitNextUpdate();
{ {
const state = getLastResultOrThrow() const state = getLastResultOrThrow();
if (state.status !== "ignored") expect.fail() if (state.status !== "ignored") expect.fail();
if (state.hook) expect.fail(); if (state.hook) expect.fail();
} }
await assertNoPendingUpdate() await assertNoPendingUpdate();
}); });
it("should render accepted if the tip has been used previously", async () => { it("should render accepted if the tip has been used previously", async () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => mountHook(() =>
useComponentState("taler://tip/asd", { useComponentState("taler://tip/asd", {
prepareTip: async () => ({ prepareTip: async () =>
({
accepted: true, accepted: true,
exchangeBaseUrl: "exchange url", exchangeBaseUrl: "exchange url",
merchantBaseUrl: "merchant url", merchantBaseUrl: "merchant url",
tipAmountEffective: "EUR:1", tipAmountEffective: "EUR:1",
walletTipId: "tip_id", walletTipId: "tip_id",
} as PrepareTipResult as any), } as PrepareTipResult as any),
acceptTip: async () => ({}) acceptTip: async () => ({}),
} as any), } as any),
); );
{ {
const { status, hook } = getLastResultOrThrow() const { status, hook } = getLastResultOrThrow();
expect(status).equals('loading') expect(status).equals("loading");
expect(hook).undefined; expect(hook).undefined;
} }
await waitNextUpdate() await waitNextUpdate();
{ {
const state = getLastResultOrThrow() const state = getLastResultOrThrow();
if (state.status !== "accepted") expect.fail() if (state.status !== "accepted") expect.fail();
if (state.hook) expect.fail(); if (state.hook) expect.fail();
expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:1")); expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:1"));
expect(state.merchantBaseUrl).eq("merchant url"); expect(state.merchantBaseUrl).eq("merchant url");
expect(state.exchangeBaseUrl).eq("exchange url"); expect(state.exchangeBaseUrl).eq("exchange url");
} }
await assertNoPendingUpdate() await assertNoPendingUpdate();
}); });
}); });

View File

@ -19,234 +19,249 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { Amounts, ExchangeListItem, GetExchangeTosResult } from "@gnu-taler/taler-util"; import {
Amounts,
ExchangeListItem,
GetExchangeTosResult,
} from "@gnu-taler/taler-util";
import { ExchangeWithdrawDetails } from "@gnu-taler/taler-wallet-core"; import { ExchangeWithdrawDetails } from "@gnu-taler/taler-wallet-core";
import { expect } from "chai"; import { expect } from "chai";
import { mountHook } from "../test-utils.js"; import { mountHook } from "../test-utils.js";
import { useComponentState } from "./Withdraw.js"; import { useComponentState } from "./Withdraw.js";
const exchanges: ExchangeListItem[] = [{ const exchanges: ExchangeListItem[] = [
currency: 'ARS', {
exchangeBaseUrl: 'http://exchange.demo.taler.net', currency: "ARS",
exchangeBaseUrl: "http://exchange.demo.taler.net",
paytoUris: [], paytoUris: [],
tos: { tos: {
acceptedVersion: '', acceptedVersion: "",
} },
}] },
];
describe("Withdraw CTA states", () => { describe("Withdraw CTA states", () => {
it("should tell the user that the URI is missing", async () => { it("should tell the user that the URI is missing", async () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() =>
useComponentState(undefined, { useComponentState(undefined, {
listExchanges: async () => ({ exchanges }), listExchanges: async () => ({ exchanges }),
getWithdrawalDetailsForUri: async ({ talerWithdrawUri }: any) => ({ getWithdrawalDetailsForUri: async ({ talerWithdrawUri }: any) => ({
amount: 'ARS:2', amount: "ARS:2",
possibleExchanges: exchanges, possibleExchanges: exchanges,
}) }),
} as any), } as any),
); );
{ {
const { status, hook } = getLastResultOrThrow() const { status, hook } = getLastResultOrThrow();
expect(status).equals('loading-uri') expect(status).equals("loading-uri");
expect(hook).undefined; expect(hook).undefined;
} }
await waitNextUpdate() await waitNextUpdate();
{ {
const { status, hook } = getLastResultOrThrow() const { status, hook } = getLastResultOrThrow();
expect(status).equals('loading-uri') expect(status).equals("loading-uri");
if (!hook) expect.fail(); if (!hook) expect.fail();
if (!hook.hasError) expect.fail(); if (!hook.hasError) expect.fail();
if (hook.operational) expect.fail(); if (hook.operational) expect.fail();
expect(hook.message).eq("ERROR_NO-URI-FOR-WITHDRAWAL"); expect(hook.message).eq("ERROR_NO-URI-FOR-WITHDRAWAL");
} }
await assertNoPendingUpdate() await assertNoPendingUpdate();
}); });
it("should tell the user that there is not known exchange", async () => { it("should tell the user that there is not known exchange", async () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
useComponentState('taler-withdraw://', { mountHook(() =>
useComponentState("taler-withdraw://", {
listExchanges: async () => ({ exchanges }), listExchanges: async () => ({ exchanges }),
getWithdrawalDetailsForUri: async ({ talerWithdrawUri }: any) => ({ getWithdrawalDetailsForUri: async ({ talerWithdrawUri }: any) => ({
amount: 'EUR:2', amount: "EUR:2",
possibleExchanges: [], possibleExchanges: [],
}) }),
} as any), } as any),
); );
{ {
const { status, hook } = getLastResultOrThrow() const { status, hook } = getLastResultOrThrow();
expect(status).equals('loading-uri') expect(status).equals("loading-uri");
expect(hook).undefined; expect(hook).undefined;
} }
await waitNextUpdate() await waitNextUpdate();
{ {
const { status, hook } = getLastResultOrThrow() const { status, hook } = getLastResultOrThrow();
expect(status).equals('loading-exchange') expect(status).equals("loading-exchange");
expect(hook).deep.equals({ "hasError": true, "operational": false, "message": "ERROR_NO-DEFAULT-EXCHANGE" }); expect(hook).deep.equals({
hasError: true,
operational: false,
message: "ERROR_NO-DEFAULT-EXCHANGE",
});
} }
await assertNoPendingUpdate() await assertNoPendingUpdate();
}); });
it("should be able to withdraw if tos are ok", async () => { it("should be able to withdraw if tos are ok", async () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
useComponentState('taler-withdraw://', { mountHook(() =>
useComponentState("taler-withdraw://", {
listExchanges: async () => ({ exchanges }), listExchanges: async () => ({ exchanges }),
getWithdrawalDetailsForUri: async ({ talerWithdrawUri }: any) => ({ getWithdrawalDetailsForUri: async ({ talerWithdrawUri }: any) => ({
amount: 'ARS:2', amount: "ARS:2",
possibleExchanges: exchanges, possibleExchanges: exchanges,
}), }),
getExchangeWithdrawalInfo: async (): Promise<ExchangeWithdrawDetails> => ({ getExchangeWithdrawalInfo:
withdrawalAmountRaw: 'ARS:5', async (): Promise<ExchangeWithdrawDetails> =>
withdrawalAmountEffective: 'ARS:5', ({
withdrawalAmountRaw: "ARS:5",
withdrawalAmountEffective: "ARS:5",
} as any), } as any),
getExchangeTos: async (): Promise<GetExchangeTosResult> => ({ getExchangeTos: async (): Promise<GetExchangeTosResult> => ({
contentType: 'text', contentType: "text",
content: 'just accept', content: "just accept",
acceptedEtag: 'v1', acceptedEtag: "v1",
currentEtag: 'v1' currentEtag: "v1",
}) }),
} as any), } as any),
); );
{ {
const { status, hook } = getLastResultOrThrow() const { status, hook } = getLastResultOrThrow();
expect(status).equals('loading-uri') expect(status).equals("loading-uri");
expect(hook).undefined; expect(hook).undefined;
} }
await waitNextUpdate() await waitNextUpdate();
{ {
const { status, hook } = getLastResultOrThrow() const { status, hook } = getLastResultOrThrow();
expect(status).equals('loading-info') expect(status).equals("loading-info");
expect(hook).undefined; expect(hook).undefined;
} }
await waitNextUpdate() await waitNextUpdate();
{ {
const state = getLastResultOrThrow() const state = getLastResultOrThrow();
expect(state.status).equals("success") expect(state.status).equals("success");
if (state.status !== "success") return; if (state.status !== "success") return;
expect(state.exchange.isDirty).false expect(state.exchange.isDirty).false;
expect(state.exchange.value).equal("http://exchange.demo.taler.net") expect(state.exchange.value).equal("http://exchange.demo.taler.net");
expect(state.exchange.list).deep.equal({ expect(state.exchange.list).deep.equal({
"http://exchange.demo.taler.net": "http://exchange.demo.taler.net" "http://exchange.demo.taler.net": "http://exchange.demo.taler.net",
}) });
expect(state.showExchangeSelection).false expect(state.showExchangeSelection).false;
expect(state.toBeReceived).deep.equal(Amounts.parseOrThrow("ARS:2")) expect(state.toBeReceived).deep.equal(Amounts.parseOrThrow("ARS:2"));
expect(state.withdrawalFee).deep.equal(Amounts.parseOrThrow("ARS:0")) expect(state.withdrawalFee).deep.equal(Amounts.parseOrThrow("ARS:0"));
expect(state.chosenAmount).deep.equal(Amounts.parseOrThrow("ARS:2")) expect(state.chosenAmount).deep.equal(Amounts.parseOrThrow("ARS:2"));
expect(state.doWithdrawal.onClick).not.undefined
expect(state.mustAcceptFirst).false
expect(state.doWithdrawal.onClick).not.undefined;
expect(state.mustAcceptFirst).false;
} }
await assertNoPendingUpdate() await assertNoPendingUpdate();
}); });
it("should be accept the tos before withdraw", async () => { it("should be accept the tos before withdraw", async () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
useComponentState('taler-withdraw://', { mountHook(() =>
useComponentState("taler-withdraw://", {
listExchanges: async () => ({ exchanges }), listExchanges: async () => ({ exchanges }),
getWithdrawalDetailsForUri: async ({ talerWithdrawUri }: any) => ({ getWithdrawalDetailsForUri: async ({ talerWithdrawUri }: any) => ({
amount: 'ARS:2', amount: "ARS:2",
possibleExchanges: exchanges, possibleExchanges: exchanges,
}), }),
getExchangeWithdrawalInfo: async (): Promise<ExchangeWithdrawDetails> => ({ getExchangeWithdrawalInfo:
withdrawalAmountRaw: 'ARS:5', async (): Promise<ExchangeWithdrawDetails> =>
withdrawalAmountEffective: 'ARS:5', ({
withdrawalAmountRaw: "ARS:5",
withdrawalAmountEffective: "ARS:5",
} as any), } as any),
getExchangeTos: async (): Promise<GetExchangeTosResult> => ({ getExchangeTos: async (): Promise<GetExchangeTosResult> => ({
contentType: 'text', contentType: "text",
content: 'just accept', content: "just accept",
acceptedEtag: 'v1', acceptedEtag: "v1",
currentEtag: 'v2' currentEtag: "v2",
}), }),
setExchangeTosAccepted: async () => ({}) setExchangeTosAccepted: async () => ({}),
} as any), } as any),
); );
{ {
const { status, hook } = getLastResultOrThrow() const { status, hook } = getLastResultOrThrow();
expect(status).equals('loading-uri') expect(status).equals("loading-uri");
expect(hook).undefined; expect(hook).undefined;
} }
await waitNextUpdate() await waitNextUpdate();
{ {
const { status, hook } = getLastResultOrThrow() const { status, hook } = getLastResultOrThrow();
expect(status).equals('loading-info') expect(status).equals("loading-info");
expect(hook).undefined; expect(hook).undefined;
} }
await waitNextUpdate() await waitNextUpdate();
{ {
const state = getLastResultOrThrow() const state = getLastResultOrThrow();
expect(state.status).equals("success") expect(state.status).equals("success");
if (state.status !== "success") return; if (state.status !== "success") return;
expect(state.exchange.isDirty).false expect(state.exchange.isDirty).false;
expect(state.exchange.value).equal("http://exchange.demo.taler.net") expect(state.exchange.value).equal("http://exchange.demo.taler.net");
expect(state.exchange.list).deep.equal({ expect(state.exchange.list).deep.equal({
"http://exchange.demo.taler.net": "http://exchange.demo.taler.net" "http://exchange.demo.taler.net": "http://exchange.demo.taler.net",
}) });
expect(state.showExchangeSelection).false expect(state.showExchangeSelection).false;
expect(state.toBeReceived).deep.equal(Amounts.parseOrThrow("ARS:2")) expect(state.toBeReceived).deep.equal(Amounts.parseOrThrow("ARS:2"));
expect(state.withdrawalFee).deep.equal(Amounts.parseOrThrow("ARS:0")) expect(state.withdrawalFee).deep.equal(Amounts.parseOrThrow("ARS:0"));
expect(state.chosenAmount).deep.equal(Amounts.parseOrThrow("ARS:2")) expect(state.chosenAmount).deep.equal(Amounts.parseOrThrow("ARS:2"));
expect(state.doWithdrawal.onClick).undefined expect(state.doWithdrawal.onClick).undefined;
expect(state.mustAcceptFirst).true expect(state.mustAcceptFirst).true;
// accept TOS // accept TOS
state.tosProps?.onAccept(true) state.tosProps?.onAccept(true);
} }
await waitNextUpdate() await waitNextUpdate();
{ {
const state = getLastResultOrThrow() const state = getLastResultOrThrow();
expect(state.status).equals("success") expect(state.status).equals("success");
if (state.status !== "success") return; if (state.status !== "success") return;
expect(state.exchange.isDirty).false expect(state.exchange.isDirty).false;
expect(state.exchange.value).equal("http://exchange.demo.taler.net") expect(state.exchange.value).equal("http://exchange.demo.taler.net");
expect(state.exchange.list).deep.equal({ expect(state.exchange.list).deep.equal({
"http://exchange.demo.taler.net": "http://exchange.demo.taler.net" "http://exchange.demo.taler.net": "http://exchange.demo.taler.net",
}) });
expect(state.showExchangeSelection).false expect(state.showExchangeSelection).false;
expect(state.toBeReceived).deep.equal(Amounts.parseOrThrow("ARS:2")) expect(state.toBeReceived).deep.equal(Amounts.parseOrThrow("ARS:2"));
expect(state.withdrawalFee).deep.equal(Amounts.parseOrThrow("ARS:0")) expect(state.withdrawalFee).deep.equal(Amounts.parseOrThrow("ARS:0"));
expect(state.chosenAmount).deep.equal(Amounts.parseOrThrow("ARS:2")) expect(state.chosenAmount).deep.equal(Amounts.parseOrThrow("ARS:2"));
expect(state.doWithdrawal.onClick).not.undefined
expect(state.mustAcceptFirst).true
expect(state.doWithdrawal.onClick).not.undefined;
expect(state.mustAcceptFirst).true;
} }
await assertNoPendingUpdate() await assertNoPendingUpdate();
}); });
}); });

View File

@ -779,4 +779,3 @@ trailer
<< /Root 3 0 R >> << /Root 3 0 R >>
%%EOF %%EOF
`; `;

View File

@ -13,7 +13,7 @@
You should have received a copy of the GNU General Public License along with You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/ */
declare module "*.jpeg" { declare module "*.jpeg" {
const content: any; const content: any;
export default content; export default content;
} }

View File

@ -13,9 +13,7 @@
You should have received a copy of the GNU General Public License along with You should have received a copy of the GNU General Public License along with
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/ */
import { import { NotificationType, TalerErrorDetail } from "@gnu-taler/taler-util";
NotificationType, TalerErrorDetail
} from "@gnu-taler/taler-util";
import { TalerError } from "@gnu-taler/taler-wallet-core"; import { TalerError } from "@gnu-taler/taler-wallet-core";
import { useEffect, useMemo, useState } from "preact/hooks"; import { useEffect, useMemo, useState } from "preact/hooks";
import * as wxApi from "../wxApi.js"; import * as wxApi from "../wxApi.js";
@ -44,19 +42,23 @@ interface WithRetry {
} }
export type HookResponse<T> = HookOk<T> | HookError | undefined; export type HookResponse<T> = HookOk<T> | HookError | undefined;
export type HookResponseWithRetry<T> = ((HookOk<T> | HookError) & WithRetry) | undefined; export type HookResponseWithRetry<T> =
| ((HookOk<T> | HookError) & WithRetry)
| undefined;
export function useAsyncAsHook<T>( export function useAsyncAsHook<T>(
fn: () => Promise<T | false>, fn: () => Promise<T | false>,
deps?: any[], deps?: any[],
): HookResponseWithRetry<T> { ): HookResponseWithRetry<T> {
const [result, setHookResponse] = useState<HookResponse<T>>(undefined); const [result, setHookResponse] = useState<HookResponse<T>>(undefined);
const args = useMemo(() => ({ const args = useMemo(
fn () => ({
fn,
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}), deps || []) }),
deps || [],
);
async function doAsync(): Promise<void> { async function doAsync(): Promise<void> {
try { try {

View File

@ -24,9 +24,9 @@ export function useExtendedPermissions(): ToggleHandler {
const [enabled, setEnabled] = useState(false); const [enabled, setEnabled] = useState(false);
const [error, setError] = useState<TalerError | undefined>(); const [error, setError] = useState<TalerError | undefined>();
const toggle = async (): Promise<void> => { const toggle = async (): Promise<void> => {
return handleExtendedPerm(enabled, setEnabled).catch(e => { return handleExtendedPerm(enabled, setEnabled).catch((e) => {
setError(TalerError.fromException(e)) setError(TalerError.fromException(e));
}) });
}; };
useEffect(() => { useEffect(() => {
@ -40,12 +40,15 @@ export function useExtendedPermissions(): ToggleHandler {
value: enabled, value: enabled,
button: { button: {
onClick: toggle, onClick: toggle,
error error,
} },
}; };
} }
async function handleExtendedPerm(isEnabled: boolean, onChange: (value: boolean) => void): Promise<void> { async function handleExtendedPerm(
isEnabled: boolean,
onChange: (value: boolean) => void,
): Promise<void> {
if (!isEnabled) { if (!isEnabled) {
// We set permissions here, since apparently FF wants this to be done // We set permissions here, since apparently FF wants this to be done
// as the result of an input event ... // as the result of an input event ...
@ -60,11 +63,10 @@ async function handleExtendedPerm(isEnabled: boolean, onChange: (value: boolean)
onChange(res.newValue); onChange(res.newValue);
} else { } else {
try { try {
await wxApi.toggleHeaderListener(false).then(r => onChange(r.newValue)); await wxApi.toggleHeaderListener(false).then((r) => onChange(r.newValue));
} catch (e) { } catch (e) {
console.log(e) console.log(e);
} }
} }
return return;
} }

View File

@ -17,12 +17,14 @@
import { useNotNullLocalStorage } from "./useLocalStorage.js"; import { useNotNullLocalStorage } from "./useLocalStorage.js";
function getBrowserLang(): string | undefined { function getBrowserLang(): string | undefined {
if (window.navigator.languages) return window.navigator.languages[0] if (window.navigator.languages) return window.navigator.languages[0];
if (window.navigator.language) return window.navigator.language if (window.navigator.language) return window.navigator.language;
return undefined; return undefined;
} }
export function useLang(initial?: string): [string, (s: string) => void, boolean] { export function useLang(
initial?: string,
): [string, (s: string) => void, boolean] {
const defaultLang = (getBrowserLang() || initial || "en").substring(0, 2); const defaultLang = (getBrowserLang() || initial || "en").substring(0, 2);
return useNotNullLocalStorage("lang-preference", defaultLang); return useNotNullLocalStorage("lang-preference", defaultLang);
} }

View File

@ -25,13 +25,13 @@ export function useLocalStorage(
key: string, key: string,
initialValue?: string, initialValue?: string,
): [string | undefined, StateUpdater<string | undefined>] { ): [string | undefined, StateUpdater<string | undefined>] {
const [storedValue, setStoredValue] = useState<string | undefined>((): const [storedValue, setStoredValue] = useState<string | undefined>(
| string (): string | undefined => {
| undefined => {
return typeof window !== "undefined" return typeof window !== "undefined"
? window.localStorage.getItem(key) || initialValue ? window.localStorage.getItem(key) || initialValue
: initialValue; : initialValue;
}); },
);
const setValue = ( const setValue = (
value?: string | ((val?: string) => string | undefined), value?: string | ((val?: string) => string | undefined),

View File

@ -14,48 +14,47 @@
You should have received a copy of the GNU General Public License along with You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/ */
import { useTalerActionURL } from "./useTalerActionURL.js" import { useTalerActionURL } from "./useTalerActionURL.js";
import { mountHook } from "../test-utils.js"; import { mountHook } from "../test-utils.js";
import { IoCProviderForTesting } from "../context/iocContext.js"; import { IoCProviderForTesting } from "../context/iocContext.js";
import { h, VNode } from "preact"; import { h, VNode } from "preact";
import { expect } from "chai"; import { expect } from "chai";
describe('useTalerActionURL hook', () => { describe("useTalerActionURL hook", () => {
it("should be set url to undefined when dismiss", async () => {
it('should be set url to undefined when dismiss', async () => {
const ctx = ({ children }: { children: any }): VNode => { const ctx = ({ children }: { children: any }): VNode => {
return h(IoCProviderForTesting, { return h(IoCProviderForTesting, {
value: { value: {
findTalerUriInActiveTab: async () => "asd", findTalerUriInActiveTab: async () => "asd",
}, children },
}) children,
} });
};
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(useTalerActionURL, ctx) const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(useTalerActionURL, ctx);
{ {
const [url] = getLastResultOrThrow() const [url] = getLastResultOrThrow();
expect(url).undefined; expect(url).undefined;
} }
await waitNextUpdate("waiting for useEffect");
await waitNextUpdate("waiting for useEffect")
{ {
const [url, setDismissed] = getLastResultOrThrow() const [url, setDismissed] = getLastResultOrThrow();
expect(url).equals("asd"); expect(url).equals("asd");
setDismissed(true) setDismissed(true);
} }
await waitNextUpdate("after dismiss") await waitNextUpdate("after dismiss");
{ {
const [url] = getLastResultOrThrow() const [url] = getLastResultOrThrow();
if (url !== undefined) throw Error('invalid') if (url !== undefined) throw Error("invalid");
expect(url).undefined; expect(url).undefined;
} }
await assertNoPendingUpdate() await assertNoPendingUpdate();
}) });
}) });

View File

@ -25,7 +25,7 @@ export function useTalerActionURL(): [
undefined, undefined,
); );
const [dismissed, setDismissed] = useState(false); const [dismissed, setDismissed] = useState(false);
const { findTalerUriInActiveTab } = useIocContext() const { findTalerUriInActiveTab } = useIocContext();
useEffect(() => { useEffect(() => {
async function check(): Promise<void> { async function check(): Promise<void> {

File diff suppressed because it is too large Load Diff

View File

@ -1,348 +1,327 @@
export const amber = { export const amber = {
50: '#fff8e1', 50: "#fff8e1",
100: '#ffecb3', 100: "#ffecb3",
200: '#ffe082', 200: "#ffe082",
300: '#ffd54f', 300: "#ffd54f",
400: '#ffca28', 400: "#ffca28",
500: '#ffc107', 500: "#ffc107",
600: '#ffb300', 600: "#ffb300",
700: '#ffa000', 700: "#ffa000",
800: '#ff8f00', 800: "#ff8f00",
900: '#ff6f00', 900: "#ff6f00",
A100: '#ffe57f', A100: "#ffe57f",
A200: '#ffd740', A200: "#ffd740",
A400: '#ffc400', A400: "#ffc400",
A700: '#ffab00', A700: "#ffab00",
}; };
export const blueGrey = { export const blueGrey = {
50: '#eceff1', 50: "#eceff1",
100: '#cfd8dc', 100: "#cfd8dc",
200: '#b0bec5', 200: "#b0bec5",
300: '#90a4ae', 300: "#90a4ae",
400: '#78909c', 400: "#78909c",
500: '#607d8b', 500: "#607d8b",
600: '#546e7a', 600: "#546e7a",
700: '#455a64', 700: "#455a64",
800: '#37474f', 800: "#37474f",
900: '#263238', 900: "#263238",
A100: '#cfd8dc', A100: "#cfd8dc",
A200: '#b0bec5', A200: "#b0bec5",
A400: '#78909c', A400: "#78909c",
A700: '#455a64', A700: "#455a64",
}; };
export const blue = { export const blue = {
50: '#e3f2fd', 50: "#e3f2fd",
100: '#bbdefb', 100: "#bbdefb",
200: '#90caf9', 200: "#90caf9",
300: '#64b5f6', 300: "#64b5f6",
400: '#42a5f5', 400: "#42a5f5",
500: '#2196f3', 500: "#2196f3",
600: '#1e88e5', 600: "#1e88e5",
700: '#1976d2', 700: "#1976d2",
800: '#1565c0', 800: "#1565c0",
900: '#0d47a1', 900: "#0d47a1",
A100: '#82b1ff', A100: "#82b1ff",
A200: '#448aff', A200: "#448aff",
A400: '#2979ff', A400: "#2979ff",
A700: '#2962ff', A700: "#2962ff",
}; };
export const brown = { export const brown = {
50: '#efebe9', 50: "#efebe9",
100: '#d7ccc8', 100: "#d7ccc8",
200: '#bcaaa4', 200: "#bcaaa4",
300: '#a1887f', 300: "#a1887f",
400: '#8d6e63', 400: "#8d6e63",
500: '#795548', 500: "#795548",
600: '#6d4c41', 600: "#6d4c41",
700: '#5d4037', 700: "#5d4037",
800: '#4e342e', 800: "#4e342e",
900: '#3e2723', 900: "#3e2723",
A100: '#d7ccc8', A100: "#d7ccc8",
A200: '#bcaaa4', A200: "#bcaaa4",
A400: '#8d6e63', A400: "#8d6e63",
A700: '#5d4037', A700: "#5d4037",
}; };
export const common = { export const common = {
black: '#000', black: "#000",
white: '#fff', white: "#fff",
}; };
export const cyan = { export const cyan = {
50: '#e0f7fa', 50: "#e0f7fa",
100: '#b2ebf2', 100: "#b2ebf2",
200: '#80deea', 200: "#80deea",
300: '#4dd0e1', 300: "#4dd0e1",
400: '#26c6da', 400: "#26c6da",
500: '#00bcd4', 500: "#00bcd4",
600: '#00acc1', 600: "#00acc1",
700: '#0097a7', 700: "#0097a7",
800: '#00838f', 800: "#00838f",
900: '#006064', 900: "#006064",
A100: '#84ffff', A100: "#84ffff",
A200: '#18ffff', A200: "#18ffff",
A400: '#00e5ff', A400: "#00e5ff",
A700: '#00b8d4', A700: "#00b8d4",
}; };
export const deepOrange = { export const deepOrange = {
50: '#fbe9e7', 50: "#fbe9e7",
100: '#ffccbc', 100: "#ffccbc",
200: '#ffab91', 200: "#ffab91",
300: '#ff8a65', 300: "#ff8a65",
400: '#ff7043', 400: "#ff7043",
500: '#ff5722', 500: "#ff5722",
600: '#f4511e', 600: "#f4511e",
700: '#e64a19', 700: "#e64a19",
800: '#d84315', 800: "#d84315",
900: '#bf360c', 900: "#bf360c",
A100: '#ff9e80', A100: "#ff9e80",
A200: '#ff6e40', A200: "#ff6e40",
A400: '#ff3d00', A400: "#ff3d00",
A700: '#dd2c00', A700: "#dd2c00",
}; };
export const deepPurple = { export const deepPurple = {
50: '#ede7f6', 50: "#ede7f6",
100: '#d1c4e9', 100: "#d1c4e9",
200: '#b39ddb', 200: "#b39ddb",
300: '#9575cd', 300: "#9575cd",
400: '#7e57c2', 400: "#7e57c2",
500: '#673ab7', 500: "#673ab7",
600: '#5e35b1', 600: "#5e35b1",
700: '#512da8', 700: "#512da8",
800: '#4527a0', 800: "#4527a0",
900: '#311b92', 900: "#311b92",
A100: '#b388ff', A100: "#b388ff",
A200: '#7c4dff', A200: "#7c4dff",
A400: '#651fff', A400: "#651fff",
A700: '#6200ea', A700: "#6200ea",
}; };
export const green = { export const green = {
50: '#e8f5e9', 50: "#e8f5e9",
100: '#c8e6c9', 100: "#c8e6c9",
200: '#a5d6a7', 200: "#a5d6a7",
300: '#81c784', 300: "#81c784",
400: '#66bb6a', 400: "#66bb6a",
500: '#4caf50', 500: "#4caf50",
600: '#43a047', 600: "#43a047",
700: '#388e3c', 700: "#388e3c",
800: '#2e7d32', 800: "#2e7d32",
900: '#1b5e20', 900: "#1b5e20",
A100: '#b9f6ca', A100: "#b9f6ca",
A200: '#69f0ae', A200: "#69f0ae",
A400: '#00e676', A400: "#00e676",
A700: '#00c853', A700: "#00c853",
}; };
export const grey = { export const grey = {
50: '#fafafa', 50: "#fafafa",
100: '#f5f5f5', 100: "#f5f5f5",
200: '#eeeeee', 200: "#eeeeee",
300: '#e0e0e0', 300: "#e0e0e0",
400: '#bdbdbd', 400: "#bdbdbd",
500: '#9e9e9e', 500: "#9e9e9e",
600: '#757575', 600: "#757575",
700: '#616161', 700: "#616161",
800: '#424242', 800: "#424242",
900: '#212121', 900: "#212121",
A100: '#f5f5f5', A100: "#f5f5f5",
A200: '#eeeeee', A200: "#eeeeee",
A400: '#bdbdbd', A400: "#bdbdbd",
A700: '#616161', A700: "#616161",
}; };
export const indigo = { export const indigo = {
50: '#e8eaf6', 50: "#e8eaf6",
100: '#c5cae9', 100: "#c5cae9",
200: '#9fa8da', 200: "#9fa8da",
300: '#7986cb', 300: "#7986cb",
400: '#5c6bc0', 400: "#5c6bc0",
500: '#3f51b5', 500: "#3f51b5",
600: '#3949ab', 600: "#3949ab",
700: '#303f9f', 700: "#303f9f",
800: '#283593', 800: "#283593",
900: '#1a237e', 900: "#1a237e",
A100: '#8c9eff', A100: "#8c9eff",
A200: '#536dfe', A200: "#536dfe",
A400: '#3d5afe', A400: "#3d5afe",
A700: '#304ffe', A700: "#304ffe",
}; };
export const lightBlue = { export const lightBlue = {
50: '#e1f5fe', 50: "#e1f5fe",
100: '#b3e5fc', 100: "#b3e5fc",
200: '#81d4fa', 200: "#81d4fa",
300: '#4fc3f7', 300: "#4fc3f7",
400: '#29b6f6', 400: "#29b6f6",
500: '#03a9f4', 500: "#03a9f4",
600: '#039be5', 600: "#039be5",
700: '#0288d1', 700: "#0288d1",
800: '#0277bd', 800: "#0277bd",
900: '#01579b', 900: "#01579b",
A100: '#80d8ff', A100: "#80d8ff",
A200: '#40c4ff', A200: "#40c4ff",
A400: '#00b0ff', A400: "#00b0ff",
A700: '#0091ea', A700: "#0091ea",
}; };
export const lightGreen = { export const lightGreen = {
50: '#f1f8e9', 50: "#f1f8e9",
100: '#dcedc8', 100: "#dcedc8",
200: '#c5e1a5', 200: "#c5e1a5",
300: '#aed581', 300: "#aed581",
400: '#9ccc65', 400: "#9ccc65",
500: '#8bc34a', 500: "#8bc34a",
600: '#7cb342', 600: "#7cb342",
700: '#689f38', 700: "#689f38",
800: '#558b2f', 800: "#558b2f",
900: '#33691e', 900: "#33691e",
A100: '#ccff90', A100: "#ccff90",
A200: '#b2ff59', A200: "#b2ff59",
A400: '#76ff03', A400: "#76ff03",
A700: '#64dd17', A700: "#64dd17",
}; };
export const lime = { export const lime = {
50: '#f9fbe7', 50: "#f9fbe7",
100: '#f0f4c3', 100: "#f0f4c3",
200: '#e6ee9c', 200: "#e6ee9c",
300: '#dce775', 300: "#dce775",
400: '#d4e157', 400: "#d4e157",
500: '#cddc39', 500: "#cddc39",
600: '#c0ca33', 600: "#c0ca33",
700: '#afb42b', 700: "#afb42b",
800: '#9e9d24', 800: "#9e9d24",
900: '#827717', 900: "#827717",
A100: '#f4ff81', A100: "#f4ff81",
A200: '#eeff41', A200: "#eeff41",
A400: '#c6ff00', A400: "#c6ff00",
A700: '#aeea00', A700: "#aeea00",
}; };
export const orange = { export const orange = {
50: '#fff3e0', 50: "#fff3e0",
100: '#ffe0b2', 100: "#ffe0b2",
200: '#ffcc80', 200: "#ffcc80",
300: '#ffb74d', 300: "#ffb74d",
400: '#ffa726', 400: "#ffa726",
500: '#ff9800', 500: "#ff9800",
600: '#fb8c00', 600: "#fb8c00",
700: '#f57c00', 700: "#f57c00",
800: '#ef6c00', 800: "#ef6c00",
900: '#e65100', 900: "#e65100",
A100: '#ffd180', A100: "#ffd180",
A200: '#ffab40', A200: "#ffab40",
A400: '#ff9100', A400: "#ff9100",
A700: '#ff6d00', A700: "#ff6d00",
}; };
export const pink = { export const pink = {
50: '#fce4ec', 50: "#fce4ec",
100: '#f8bbd0', 100: "#f8bbd0",
200: '#f48fb1', 200: "#f48fb1",
300: '#f06292', 300: "#f06292",
400: '#ec407a', 400: "#ec407a",
500: '#e91e63', 500: "#e91e63",
600: '#d81b60', 600: "#d81b60",
700: '#c2185b', 700: "#c2185b",
800: '#ad1457', 800: "#ad1457",
900: '#880e4f', 900: "#880e4f",
A100: '#ff80ab', A100: "#ff80ab",
A200: '#ff4081', A200: "#ff4081",
A400: '#f50057', A400: "#f50057",
A700: '#c51162', A700: "#c51162",
}; };
export const purple = { export const purple = {
50: '#f3e5f5', 50: "#f3e5f5",
100: '#e1bee7', 100: "#e1bee7",
200: '#ce93d8', 200: "#ce93d8",
300: '#ba68c8', 300: "#ba68c8",
400: '#ab47bc', 400: "#ab47bc",
500: '#9c27b0', 500: "#9c27b0",
600: '#8e24aa', 600: "#8e24aa",
700: '#7b1fa2', 700: "#7b1fa2",
800: '#6a1b9a', 800: "#6a1b9a",
900: '#4a148c', 900: "#4a148c",
A100: '#ea80fc', A100: "#ea80fc",
A200: '#e040fb', A200: "#e040fb",
A400: '#d500f9', A400: "#d500f9",
A700: '#aa00ff', A700: "#aa00ff",
}; };
export const red = { export const red = {
50: '#ffebee', 50: "#ffebee",
100: '#ffcdd2', 100: "#ffcdd2",
200: '#ef9a9a', 200: "#ef9a9a",
300: '#e57373', 300: "#e57373",
400: '#ef5350', 400: "#ef5350",
500: '#f44336', 500: "#f44336",
600: '#e53935', 600: "#e53935",
700: '#d32f2f', 700: "#d32f2f",
800: '#c62828', 800: "#c62828",
900: '#b71c1c', 900: "#b71c1c",
A100: '#ff8a80', A100: "#ff8a80",
A200: '#ff5252', A200: "#ff5252",
A400: '#ff1744', A400: "#ff1744",
A700: '#d50000', A700: "#d50000",
}; };
export const teal = { export const teal = {
50: '#e0f2f1', 50: "#e0f2f1",
100: '#b2dfdb', 100: "#b2dfdb",
200: '#80cbc4', 200: "#80cbc4",
300: '#4db6ac', 300: "#4db6ac",
400: '#26a69a', 400: "#26a69a",
500: '#009688', 500: "#009688",
600: '#00897b', 600: "#00897b",
700: '#00796b', 700: "#00796b",
800: '#00695c', 800: "#00695c",
900: '#004d40', 900: "#004d40",
A100: '#a7ffeb', A100: "#a7ffeb",
A200: '#64ffda', A200: "#64ffda",
A400: '#1de9b6', A400: "#1de9b6",
A700: '#00bfa5', A700: "#00bfa5",
}; };
export const yellow = { export const yellow = {
50: '#fffde7', 50: "#fffde7",
100: '#fff9c4', 100: "#fff9c4",
200: '#fff59d', 200: "#fff59d",
300: '#fff176', 300: "#fff176",
400: '#ffee58', 400: "#ffee58",
500: '#ffeb3b', 500: "#ffeb3b",
600: '#fdd835', 600: "#fdd835",
700: '#fbc02d', 700: "#fbc02d",
800: '#f9a825', 800: "#f9a825",
900: '#f57f17', 900: "#f57f17",
A100: '#ffff8d', A100: "#ffff8d",
A200: '#ffff00', A200: "#ffff00",
A400: '#ffea00', A400: "#ffea00",
A700: '#ffd600', A700: "#ffd600",
}; };

View File

@ -1,4 +1,4 @@
import { expect } from 'chai'; import { expect } from "chai";
import { import {
recomposeColor, recomposeColor,
hexToRgb, hexToRgb,
@ -11,295 +11,308 @@ import {
getContrastRatio, getContrastRatio,
getLuminance, getLuminance,
lighten, lighten,
} from './manipulation.js'; } from "./manipulation.js";
describe('utils/colorManipulator', () => { describe("utils/colorManipulator", () => {
describe('recomposeColor', () => { describe("recomposeColor", () => {
it('converts a decomposed rgb color object to a string` ', () => { it("converts a decomposed rgb color object to a string` ", () => {
expect( expect(
recomposeColor({ recomposeColor({
type: 'rgb', type: "rgb",
values: [255, 255, 255], values: [255, 255, 255],
}), }),
).to.equal('rgb(255, 255, 255)'); ).to.equal("rgb(255, 255, 255)");
}); });
it('converts a decomposed rgba color object to a string` ', () => { it("converts a decomposed rgba color object to a string` ", () => {
expect( expect(
recomposeColor({ recomposeColor({
type: 'rgba', type: "rgba",
values: [255, 255, 255, 0.5], values: [255, 255, 255, 0.5],
}), }),
).to.equal('rgba(255, 255, 255, 0.5)'); ).to.equal("rgba(255, 255, 255, 0.5)");
}); });
it('converts a decomposed hsl color object to a string` ', () => { it("converts a decomposed hsl color object to a string` ", () => {
expect( expect(
recomposeColor({ recomposeColor({
type: 'hsl', type: "hsl",
values: [100, 50, 25], values: [100, 50, 25],
}), }),
).to.equal('hsl(100, 50%, 25%)'); ).to.equal("hsl(100, 50%, 25%)");
}); });
it('converts a decomposed hsla color object to a string` ', () => { it("converts a decomposed hsla color object to a string` ", () => {
expect( expect(
recomposeColor({ recomposeColor({
type: 'hsla', type: "hsla",
values: [100, 50, 25, 0.5], values: [100, 50, 25, 0.5],
}), }),
).to.equal('hsla(100, 50%, 25%, 0.5)'); ).to.equal("hsla(100, 50%, 25%, 0.5)");
}); });
}); });
describe('hexToRgb', () => { describe("hexToRgb", () => {
it('converts a short hex color to an rgb color` ', () => { it("converts a short hex color to an rgb color` ", () => {
expect(hexToRgb('#9f3')).to.equal('rgb(153, 255, 51)'); expect(hexToRgb("#9f3")).to.equal("rgb(153, 255, 51)");
}); });
it('converts a long hex color to an rgb color` ', () => { it("converts a long hex color to an rgb color` ", () => {
expect(hexToRgb('#a94fd3')).to.equal('rgb(169, 79, 211)'); expect(hexToRgb("#a94fd3")).to.equal("rgb(169, 79, 211)");
}); });
it('converts a long alpha hex color to an argb color` ', () => { it("converts a long alpha hex color to an argb color` ", () => {
expect(hexToRgb('#111111f8')).to.equal('rgba(17, 17, 17, 0.973)'); expect(hexToRgb("#111111f8")).to.equal("rgba(17, 17, 17, 0.973)");
}); });
}); });
describe('rgbToHex', () => { describe("rgbToHex", () => {
it('converts an rgb color to a hex color` ', () => { it("converts an rgb color to a hex color` ", () => {
expect(rgbToHex('rgb(169, 79, 211)')).to.equal('#a94fd3'); expect(rgbToHex("rgb(169, 79, 211)")).to.equal("#a94fd3");
}); });
it('converts an rgba color to a hex color` ', () => { it("converts an rgba color to a hex color` ", () => {
expect(rgbToHex('rgba(169, 79, 211, 1)')).to.equal('#a94fd3ff'); expect(rgbToHex("rgba(169, 79, 211, 1)")).to.equal("#a94fd3ff");
}); });
it('idempotent', () => { it("idempotent", () => {
expect(rgbToHex('#A94FD3')).to.equal('#A94FD3'); expect(rgbToHex("#A94FD3")).to.equal("#A94FD3");
}); });
}); });
describe('hslToRgb', () => { describe("hslToRgb", () => {
it('converts an hsl color to an rgb color` ', () => { it("converts an hsl color to an rgb color` ", () => {
expect(hslToRgb('hsl(281, 60%, 57%)')).to.equal('rgb(169, 80, 211)'); expect(hslToRgb("hsl(281, 60%, 57%)")).to.equal("rgb(169, 80, 211)");
}); });
it('converts an hsla color to an rgba color` ', () => { it("converts an hsla color to an rgba color` ", () => {
expect(hslToRgb('hsla(281, 60%, 57%, 0.5)')).to.equal('rgba(169, 80, 211, 0.5)'); expect(hslToRgb("hsla(281, 60%, 57%, 0.5)")).to.equal(
"rgba(169, 80, 211, 0.5)",
);
}); });
it('allow to convert values only', () => { it("allow to convert values only", () => {
expect(hslToRgb('hsl(281, 60%, 57%)')).to.equal('rgb(169, 80, 211)'); expect(hslToRgb("hsl(281, 60%, 57%)")).to.equal("rgb(169, 80, 211)");
}); });
}); });
describe('decomposeColor', () => { describe("decomposeColor", () => {
it('converts an rgb color string to an object with `type` and `value` keys', () => { it("converts an rgb color string to an object with `type` and `value` keys", () => {
const { type, values } = decomposeColor('rgb(255, 255, 255)'); const { type, values } = decomposeColor("rgb(255, 255, 255)");
expect(type).to.equal('rgb'); expect(type).to.equal("rgb");
expect(values).to.deep.equal([255, 255, 255]); expect(values).to.deep.equal([255, 255, 255]);
}); });
it('converts an rgba color string to an object with `type` and `value` keys', () => { it("converts an rgba color string to an object with `type` and `value` keys", () => {
const { type, values } = decomposeColor('rgba(255, 255, 255, 0.5)'); const { type, values } = decomposeColor("rgba(255, 255, 255, 0.5)");
expect(type).to.equal('rgba'); expect(type).to.equal("rgba");
expect(values).to.deep.equal([255, 255, 255, 0.5]); expect(values).to.deep.equal([255, 255, 255, 0.5]);
}); });
it('converts an hsl color string to an object with `type` and `value` keys', () => { it("converts an hsl color string to an object with `type` and `value` keys", () => {
const { type, values } = decomposeColor('hsl(100, 50%, 25%)'); const { type, values } = decomposeColor("hsl(100, 50%, 25%)");
expect(type).to.equal('hsl'); expect(type).to.equal("hsl");
expect(values).to.deep.equal([100, 50, 25]); expect(values).to.deep.equal([100, 50, 25]);
}); });
it('converts an hsla color string to an object with `type` and `value` keys', () => { it("converts an hsla color string to an object with `type` and `value` keys", () => {
const { type, values } = decomposeColor('hsla(100, 50%, 25%, 0.5)'); const { type, values } = decomposeColor("hsla(100, 50%, 25%, 0.5)");
expect(type).to.equal('hsla'); expect(type).to.equal("hsla");
expect(values).to.deep.equal([100, 50, 25, 0.5]); expect(values).to.deep.equal([100, 50, 25, 0.5]);
}); });
it('converts rgba hex', () => { it("converts rgba hex", () => {
const decomposed = decomposeColor('#111111f8'); const decomposed = decomposeColor("#111111f8");
expect(decomposed).to.deep.equal({ expect(decomposed).to.deep.equal({
type: 'rgba', type: "rgba",
colorSpace: undefined, colorSpace: undefined,
values: [17, 17, 17, 0.973], values: [17, 17, 17, 0.973],
}); });
}); });
}); });
describe('getContrastRatio', () => { describe("getContrastRatio", () => {
it('returns a ratio for black : white', () => { it("returns a ratio for black : white", () => {
expect(getContrastRatio('#000', '#FFF')).to.equal(21); expect(getContrastRatio("#000", "#FFF")).to.equal(21);
}); });
it('returns a ratio for black : black', () => { it("returns a ratio for black : black", () => {
expect(getContrastRatio('#000', '#000')).to.equal(1); expect(getContrastRatio("#000", "#000")).to.equal(1);
}); });
it('returns a ratio for white : white', () => { it("returns a ratio for white : white", () => {
expect(getContrastRatio('#FFF', '#FFF')).to.equal(1); expect(getContrastRatio("#FFF", "#FFF")).to.equal(1);
}); });
it('returns a ratio for dark-grey : light-grey', () => { it("returns a ratio for dark-grey : light-grey", () => {
expect(getContrastRatio('#707070', '#E5E5E5')).to.be.approximately(3.93, 0.01); expect(getContrastRatio("#707070", "#E5E5E5")).to.be.approximately(
3.93,
0.01,
);
}); });
it('returns a ratio for black : light-grey', () => { it("returns a ratio for black : light-grey", () => {
expect(getContrastRatio('#000', '#888')).to.be.approximately(5.92, 0.01); expect(getContrastRatio("#000", "#888")).to.be.approximately(5.92, 0.01);
}); });
}); });
describe('getLuminance', () => { describe("getLuminance", () => {
it("returns a valid luminance for rgb white ", () => {
it('returns a valid luminance for rgb white ', () => { expect(getLuminance("rgba(255, 255, 255)")).to.equal(1);
expect(getLuminance('rgba(255, 255, 255)')).to.equal(1); expect(getLuminance("rgb(255, 255, 255)")).to.equal(1);
expect(getLuminance('rgb(255, 255, 255)')).to.equal(1);
}); });
it('returns a valid luminance for rgb mid-grey', () => { it("returns a valid luminance for rgb mid-grey", () => {
expect(getLuminance('rgba(127, 127, 127)')).to.equal(0.212); expect(getLuminance("rgba(127, 127, 127)")).to.equal(0.212);
expect(getLuminance('rgb(127, 127, 127)')).to.equal(0.212); expect(getLuminance("rgb(127, 127, 127)")).to.equal(0.212);
}); });
it('returns a valid luminance for an rgb color', () => { it("returns a valid luminance for an rgb color", () => {
expect(getLuminance('rgb(255, 127, 0)')).to.equal(0.364); expect(getLuminance("rgb(255, 127, 0)")).to.equal(0.364);
}); });
it('returns a valid luminance from an hsl color', () => { it("returns a valid luminance from an hsl color", () => {
expect(getLuminance('hsl(100, 100%, 50%)')).to.equal(0.735); expect(getLuminance("hsl(100, 100%, 50%)")).to.equal(0.735);
}); });
it('returns an equal luminance for the same color in different formats', () => { it("returns an equal luminance for the same color in different formats", () => {
const hsl = 'hsl(100, 100%, 50%)'; const hsl = "hsl(100, 100%, 50%)";
const rgb = 'rgb(85, 255, 0)'; const rgb = "rgb(85, 255, 0)";
expect(getLuminance(hsl)).to.equal(getLuminance(rgb)); expect(getLuminance(hsl)).to.equal(getLuminance(rgb));
}); });
}); });
describe('emphasize', () => { describe("emphasize", () => {
it('lightens a dark rgb color with the coefficient provided', () => { it("lightens a dark rgb color with the coefficient provided", () => {
expect(emphasize('rgb(1, 2, 3)', 0.4)).to.equal(lighten('rgb(1, 2, 3)', 0.4)); expect(emphasize("rgb(1, 2, 3)", 0.4)).to.equal(
lighten("rgb(1, 2, 3)", 0.4),
);
}); });
it('darkens a light rgb color with the coefficient provided', () => { it("darkens a light rgb color with the coefficient provided", () => {
expect(emphasize('rgb(250, 240, 230)', 0.3)).to.equal(darken('rgb(250, 240, 230)', 0.3)); expect(emphasize("rgb(250, 240, 230)", 0.3)).to.equal(
darken("rgb(250, 240, 230)", 0.3),
);
}); });
it('lightens a dark rgb color with the coefficient 0.15 by default', () => { it("lightens a dark rgb color with the coefficient 0.15 by default", () => {
expect(emphasize('rgb(1, 2, 3)')).to.equal(lighten('rgb(1, 2, 3)', 0.15)); expect(emphasize("rgb(1, 2, 3)")).to.equal(lighten("rgb(1, 2, 3)", 0.15));
}); });
it('darkens a light rgb color with the coefficient 0.15 by default', () => { it("darkens a light rgb color with the coefficient 0.15 by default", () => {
expect(emphasize('rgb(250, 240, 230)')).to.equal(darken('rgb(250, 240, 230)', 0.15)); expect(emphasize("rgb(250, 240, 230)")).to.equal(
darken("rgb(250, 240, 230)", 0.15),
);
});
}); });
describe("alpha", () => {
it("converts an rgb color to an rgba color with the value provided", () => {
expect(alpha("rgb(1, 2, 3)", 0.4)).to.equal("rgba(1, 2, 3, 0.4)");
}); });
describe('alpha', () => { it("updates an rgba color with the alpha value provided", () => {
it('converts an rgb color to an rgba color with the value provided', () => { expect(alpha("rgba(255, 0, 0, 0.2)", 0.5)).to.equal(
expect(alpha('rgb(1, 2, 3)', 0.4)).to.equal('rgba(1, 2, 3, 0.4)'); "rgba(255, 0, 0, 0.5)",
);
}); });
it('updates an rgba color with the alpha value provided', () => { it("converts an hsl color to an hsla color with the value provided", () => {
expect(alpha('rgba(255, 0, 0, 0.2)', 0.5)).to.equal('rgba(255, 0, 0, 0.5)'); expect(alpha("hsl(0, 100%, 50%)", 0.1)).to.equal(
"hsla(0, 100%, 50%, 0.1)",
);
}); });
it('converts an hsl color to an hsla color with the value provided', () => { it("updates an hsla color with the alpha value provided", () => {
expect(alpha('hsl(0, 100%, 50%)', 0.1)).to.equal('hsla(0, 100%, 50%, 0.1)'); expect(alpha("hsla(0, 100%, 50%, 0.2)", 0.5)).to.equal(
"hsla(0, 100%, 50%, 0.5)",
);
});
}); });
it('updates an hsla color with the alpha value provided', () => { describe("darken", () => {
expect(alpha('hsla(0, 100%, 50%, 0.2)', 0.5)).to.equal('hsla(0, 100%, 50%, 0.5)');
});
});
describe('darken', () => {
it("doesn't modify rgb black", () => { it("doesn't modify rgb black", () => {
expect(darken('rgb(0, 0, 0)', 0.1)).to.equal('rgb(0, 0, 0)'); expect(darken("rgb(0, 0, 0)", 0.1)).to.equal("rgb(0, 0, 0)");
}); });
it('darkens rgb white to black when coefficient is 1', () => { it("darkens rgb white to black when coefficient is 1", () => {
expect(darken('rgb(255, 255, 255)', 1)).to.equal('rgb(0, 0, 0)'); expect(darken("rgb(255, 255, 255)", 1)).to.equal("rgb(0, 0, 0)");
}); });
it('retains the alpha value in an rgba color', () => { it("retains the alpha value in an rgba color", () => {
expect(darken('rgba(0, 0, 0, 0.5)', 0.1)).to.equal('rgba(0, 0, 0, 0.5)'); expect(darken("rgba(0, 0, 0, 0.5)", 0.1)).to.equal("rgba(0, 0, 0, 0.5)");
}); });
it('darkens rgb white by 10% when coefficient is 0.1', () => { it("darkens rgb white by 10% when coefficient is 0.1", () => {
expect(darken('rgb(255, 255, 255)', 0.1)).to.equal('rgb(229, 229, 229)'); expect(darken("rgb(255, 255, 255)", 0.1)).to.equal("rgb(229, 229, 229)");
}); });
it('darkens rgb red by 50% when coefficient is 0.5', () => { it("darkens rgb red by 50% when coefficient is 0.5", () => {
expect(darken('rgb(255, 0, 0)', 0.5)).to.equal('rgb(127, 0, 0)'); expect(darken("rgb(255, 0, 0)", 0.5)).to.equal("rgb(127, 0, 0)");
}); });
it('darkens rgb grey by 50% when coefficient is 0.5', () => { it("darkens rgb grey by 50% when coefficient is 0.5", () => {
expect(darken('rgb(127, 127, 127)', 0.5)).to.equal('rgb(63, 63, 63)'); expect(darken("rgb(127, 127, 127)", 0.5)).to.equal("rgb(63, 63, 63)");
}); });
it("doesn't modify rgb colors when coefficient is 0", () => { it("doesn't modify rgb colors when coefficient is 0", () => {
expect(darken('rgb(255, 255, 255)', 0)).to.equal('rgb(255, 255, 255)'); expect(darken("rgb(255, 255, 255)", 0)).to.equal("rgb(255, 255, 255)");
}); });
it('darkens hsl red by 50% when coefficient is 0.5', () => { it("darkens hsl red by 50% when coefficient is 0.5", () => {
expect(darken('hsl(0, 100%, 50%)', 0.5)).to.equal('hsl(0, 100%, 25%)'); expect(darken("hsl(0, 100%, 50%)", 0.5)).to.equal("hsl(0, 100%, 25%)");
}); });
it("doesn't modify hsl colors when coefficient is 0", () => { it("doesn't modify hsl colors when coefficient is 0", () => {
expect(darken('hsl(0, 100%, 50%)', 0)).to.equal('hsl(0, 100%, 50%)'); expect(darken("hsl(0, 100%, 50%)", 0)).to.equal("hsl(0, 100%, 50%)");
}); });
it("doesn't modify hsl colors when l is 0%", () => { it("doesn't modify hsl colors when l is 0%", () => {
expect(darken('hsl(0, 50%, 0%)', 0.5)).to.equal('hsl(0, 50%, 0%)'); expect(darken("hsl(0, 50%, 0%)", 0.5)).to.equal("hsl(0, 50%, 0%)");
});
}); });
}); describe("lighten", () => {
describe('lighten', () => {
it("doesn't modify rgb white", () => { it("doesn't modify rgb white", () => {
expect(lighten('rgb(255, 255, 255)', 0.1)).to.equal('rgb(255, 255, 255)'); expect(lighten("rgb(255, 255, 255)", 0.1)).to.equal("rgb(255, 255, 255)");
}); });
it('lightens rgb black to white when coefficient is 1', () => { it("lightens rgb black to white when coefficient is 1", () => {
expect(lighten('rgb(0, 0, 0)', 1)).to.equal('rgb(255, 255, 255)'); expect(lighten("rgb(0, 0, 0)", 1)).to.equal("rgb(255, 255, 255)");
}); });
it('retains the alpha value in an rgba color', () => { it("retains the alpha value in an rgba color", () => {
expect(lighten('rgba(255, 255, 255, 0.5)', 0.1)).to.equal('rgba(255, 255, 255, 0.5)'); expect(lighten("rgba(255, 255, 255, 0.5)", 0.1)).to.equal(
"rgba(255, 255, 255, 0.5)",
);
}); });
it('lightens rgb black by 10% when coefficient is 0.1', () => { it("lightens rgb black by 10% when coefficient is 0.1", () => {
expect(lighten('rgb(0, 0, 0)', 0.1)).to.equal('rgb(25, 25, 25)'); expect(lighten("rgb(0, 0, 0)", 0.1)).to.equal("rgb(25, 25, 25)");
}); });
it('lightens rgb red by 50% when coefficient is 0.5', () => { it("lightens rgb red by 50% when coefficient is 0.5", () => {
expect(lighten('rgb(255, 0, 0)', 0.5)).to.equal('rgb(255, 127, 127)'); expect(lighten("rgb(255, 0, 0)", 0.5)).to.equal("rgb(255, 127, 127)");
}); });
it('lightens rgb grey by 50% when coefficient is 0.5', () => { it("lightens rgb grey by 50% when coefficient is 0.5", () => {
expect(lighten('rgb(127, 127, 127)', 0.5)).to.equal('rgb(191, 191, 191)'); expect(lighten("rgb(127, 127, 127)", 0.5)).to.equal("rgb(191, 191, 191)");
}); });
it("doesn't modify rgb colors when coefficient is 0", () => { it("doesn't modify rgb colors when coefficient is 0", () => {
expect(lighten('rgb(127, 127, 127)', 0)).to.equal('rgb(127, 127, 127)'); expect(lighten("rgb(127, 127, 127)", 0)).to.equal("rgb(127, 127, 127)");
}); });
it('lightens hsl red by 50% when coefficient is 0.5', () => { it("lightens hsl red by 50% when coefficient is 0.5", () => {
expect(lighten('hsl(0, 100%, 50%)', 0.5)).to.equal('hsl(0, 100%, 75%)'); expect(lighten("hsl(0, 100%, 50%)", 0.5)).to.equal("hsl(0, 100%, 75%)");
}); });
it("doesn't modify hsl colors when coefficient is 0", () => { it("doesn't modify hsl colors when coefficient is 0", () => {
expect(lighten('hsl(0, 100%, 50%)', 0)).to.equal('hsl(0, 100%, 50%)'); expect(lighten("hsl(0, 100%, 50%)", 0)).to.equal("hsl(0, 100%, 50%)");
}); });
it("doesn't modify hsl colors when `l` is 100%", () => { it("doesn't modify hsl colors when `l` is 100%", () => {
expect(lighten('hsl(0, 50%, 100%)', 0.5)).to.equal('hsl(0, 50%, 100%)'); expect(lighten("hsl(0, 50%, 100%)", 0.5)).to.equal("hsl(0, 50%, 100%)");
}); });
}); });
}); });

View File

@ -1,20 +1,18 @@
export type ColorFormat = ColorFormatWithAlpha | ColorFormatWithoutAlpha;
export type ColorFormat = ColorFormatWithAlpha | ColorFormatWithoutAlpha export type ColorFormatWithAlpha = "rgb" | "hsl";
export type ColorFormatWithAlpha = 'rgb' | 'hsl'; export type ColorFormatWithoutAlpha = "rgba" | "hsla";
export type ColorFormatWithoutAlpha = 'rgba' | 'hsla'; export type ColorObject = ColorObjectWithAlpha | ColorObjectWithoutAlpha;
export type ColorObject = ColorObjectWithAlpha | ColorObjectWithoutAlpha
export interface ColorObjectWithAlpha { export interface ColorObjectWithAlpha {
type: ColorFormatWithAlpha; type: ColorFormatWithAlpha;
values: [number, number, number]; values: [number, number, number];
colorSpace?: 'srgb' | 'display-p3' | 'a98-rgb' | 'prophoto-rgb' | 'rec-2020'; colorSpace?: "srgb" | "display-p3" | "a98-rgb" | "prophoto-rgb" | "rec-2020";
} }
export interface ColorObjectWithoutAlpha { export interface ColorObjectWithoutAlpha {
type: ColorFormatWithoutAlpha; type: ColorFormatWithoutAlpha;
values: [number, number, number, number]; values: [number, number, number, number];
colorSpace?: 'srgb' | 'display-p3' | 'a98-rgb' | 'prophoto-rgb' | 'rec-2020'; colorSpace?: "srgb" | "display-p3" | "a98-rgb" | "prophoto-rgb" | "rec-2020";
} }
/** /**
* Returns a number whose value is limited to the given range. * Returns a number whose value is limited to the given range.
* @param {number} value The value to be clamped * @param {number} value The value to be clamped
@ -40,7 +38,7 @@ function clamp(value: number, min = 0, max = 1): number {
export function hexToRgb(color: string): string { export function hexToRgb(color: string): string {
color = color.substr(1); color = color.substr(1);
const re = new RegExp(`.{1,${color.length >= 6 ? 2 : 1}}`, 'g'); const re = new RegExp(`.{1,${color.length >= 6 ? 2 : 1}}`, "g");
let colors = color.match(re); let colors = color.match(re);
if (colors && colors[0].length === 1) { if (colors && colors[0].length === 1) {
@ -48,12 +46,14 @@ export function hexToRgb(color: string): string {
} }
return colors return colors
? `rgb${colors.length === 4 ? 'a' : ''}(${colors ? `rgb${colors.length === 4 ? "a" : ""}(${colors
.map((n, index) => { .map((n, index) => {
return index < 3 ? parseInt(n, 16) : Math.round((parseInt(n, 16) / 255) * 1000) / 1000; return index < 3
? parseInt(n, 16)
: Math.round((parseInt(n, 16) / 255) * 1000) / 1000;
}) })
.join(', ')})` .join(", ")})`
: ''; : "";
} }
function intToHex(int: number): string { function intToHex(int: number): string {
@ -70,23 +70,42 @@ function intToHex(int: number): string {
*/ */
export function decomposeColor(color: string): ColorObject { export function decomposeColor(color: string): ColorObject {
const colorSpace = undefined; const colorSpace = undefined;
if (color.charAt(0) === '#') { if (color.charAt(0) === "#") {
return decomposeColor(hexToRgb(color)); return decomposeColor(hexToRgb(color));
} }
const marker = color.indexOf('('); const marker = color.indexOf("(");
const type = color.substring(0, marker); const type = color.substring(0, marker);
// if (type != 'rgba' && type != 'hsla' && type != 'rgb' && type != 'hsl') { // if (type != 'rgba' && type != 'hsla' && type != 'rgb' && type != 'hsl') {
// } // }
const values = color.substring(marker + 1, color.length - 1).split(',') const values = color.substring(marker + 1, color.length - 1).split(",");
if (type == 'rgb' || type == 'hsl') { if (type == "rgb" || type == "hsl") {
return { type, colorSpace, values: [parseFloat(values[0]), parseFloat(values[1]), parseFloat(values[2])] } return {
type,
colorSpace,
values: [
parseFloat(values[0]),
parseFloat(values[1]),
parseFloat(values[2]),
],
};
} }
if (type == 'rgba' || type == 'hsla') { if (type == "rgba" || type == "hsla") {
return { type, colorSpace, values: [parseFloat(values[0]), parseFloat(values[1]), parseFloat(values[2]), parseFloat(values[3])] } return {
type,
colorSpace,
values: [
parseFloat(values[0]),
parseFloat(values[1]),
parseFloat(values[2]),
parseFloat(values[3]),
],
};
} }
throw new Error(`Unsupported '${color}' color. The following formats are supported: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla()`) throw new Error(
`Unsupported '${color}' color. The following formats are supported: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla()`,
);
} }
/** /**
@ -100,19 +119,21 @@ export function recomposeColor(color: ColorObject): string {
const { type, values: valuesNum } = color; const { type, values: valuesNum } = color;
const valuesStr: string[] = []; const valuesStr: string[] = [];
if (type.indexOf('rgb') !== -1) { if (type.indexOf("rgb") !== -1) {
// Only convert the first 3 values to int (i.e. not alpha) // Only convert the first 3 values to int (i.e. not alpha)
valuesNum.map((n, i) => (i < 3 ? parseInt(String(n), 10) : n)).forEach((n, i) => valuesStr[i] = String(n)); valuesNum
} else if (type.indexOf('hsl') !== -1) { .map((n, i) => (i < 3 ? parseInt(String(n), 10) : n))
valuesStr[0] = String(valuesNum[0]) .forEach((n, i) => (valuesStr[i] = String(n)));
} else if (type.indexOf("hsl") !== -1) {
valuesStr[0] = String(valuesNum[0]);
valuesStr[1] = `${valuesNum[1]}%`; valuesStr[1] = `${valuesNum[1]}%`;
valuesStr[2] = `${valuesNum[2]}%`; valuesStr[2] = `${valuesNum[2]}%`;
if (type === 'hsla') { if (type === "hsla") {
valuesStr[3] = String(valuesNum[3]) valuesStr[3] = String(valuesNum[3]);
} }
} }
return `${type}(${valuesStr.join(', ')})`; return `${type}(${valuesStr.join(", ")})`;
} }
/** /**
@ -122,12 +143,14 @@ export function recomposeColor(color: ColorObject): string {
*/ */
export function rgbToHex(color: string): string { export function rgbToHex(color: string): string {
// Idempotent // Idempotent
if (color.indexOf('#') === 0) { if (color.indexOf("#") === 0) {
return color; return color;
} }
const { values } = decomposeColor(color); const { values } = decomposeColor(color);
return `#${values.map((n, i) => intToHex(i === 3 ? Math.round(255 * n) : n)).join('')}`; return `#${values
.map((n, i) => intToHex(i === 3 ? Math.round(255 * n) : n))
.join("")}`;
} }
/** /**
@ -142,24 +165,28 @@ export function hslToRgb(color: string): string {
const s = values[1] / 100; const s = values[1] / 100;
const l = values[2] / 100; const l = values[2] / 100;
const a = s * Math.min(l, 1 - l); const a = s * Math.min(l, 1 - l);
const f = (n: number, k = (n + h / 30) % 12): number => l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1); const f = (n: number, k = (n + h / 30) % 12): number =>
l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
if (colorObj.type === 'hsla') { if (colorObj.type === "hsla") {
return recomposeColor({ return recomposeColor({
type: 'rgba', values: [ type: "rgba",
values: [
Math.round(f(0) * 255), Math.round(f(0) * 255),
Math.round(f(8) * 255), Math.round(f(8) * 255),
Math.round(f(4) * 255), Math.round(f(4) * 255),
colorObj.values[3] colorObj.values[3],
] ],
}) });
} }
return recomposeColor({ return recomposeColor({
type: 'rgb', values: [ type: "rgb",
values: [
Math.round(f(0) * 255), Math.round(f(0) * 255),
Math.round(f(8) * 255), Math.round(f(8) * 255),
Math.round(f(4) * 255)] Math.round(f(4) * 255),
],
}); });
} }
/** /**
@ -173,14 +200,19 @@ export function hslToRgb(color: string): string {
export function getLuminance(color: string): number { export function getLuminance(color: string): number {
const colorObj = decomposeColor(color); const colorObj = decomposeColor(color);
const rgb2 = colorObj.type === 'hsl' ? decomposeColor(hslToRgb(color)).values : colorObj.values; const rgb2 =
colorObj.type === "hsl"
? decomposeColor(hslToRgb(color)).values
: colorObj.values;
const rgb = rgb2.map((val) => { const rgb = rgb2.map((val) => {
val /= 255; // normalized val /= 255; // normalized
return val <= 0.03928 ? val / 12.92 : ((val + 0.055) / 1.055) ** 2.4; return val <= 0.03928 ? val / 12.92 : ((val + 0.055) / 1.055) ** 2.4;
}) as typeof rgb2; }) as typeof rgb2;
// Truncate at 3 digits // Truncate at 3 digits
return Number((0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2]).toFixed(3)); return Number(
(0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2]).toFixed(3),
);
} }
/** /**
@ -191,7 +223,10 @@ export function getLuminance(color: string): number {
* @param {string} background - CSS color, i.e. one of: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla() * @param {string} background - CSS color, i.e. one of: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla()
* @returns {number} A contrast ratio value in the range 0 - 21. * @returns {number} A contrast ratio value in the range 0 - 21.
*/ */
export function getContrastRatio(foreground: string, background: string): number { export function getContrastRatio(
foreground: string,
background: string,
): number {
const lumA = getLuminance(foreground); const lumA = getLuminance(foreground);
const lumB = getLuminance(background); const lumB = getLuminance(background);
return (Math.max(lumA, lumB) + 0.05) / (Math.min(lumA, lumB) + 0.05); return (Math.max(lumA, lumB) + 0.05) / (Math.min(lumA, lumB) + 0.05);
@ -208,8 +243,8 @@ export function alpha(color: string, value: number): string {
const colorObj = decomposeColor(color); const colorObj = decomposeColor(color);
value = clamp(value); value = clamp(value);
if (colorObj.type === 'rgb' || colorObj.type === 'hsl') { if (colorObj.type === "rgb" || colorObj.type === "hsl") {
colorObj.type += 'a'; colorObj.type += "a";
} }
colorObj.values[3] = value; colorObj.values[3] = value;
@ -226,9 +261,12 @@ export function darken(color: string, coefficient: number): string {
const colorObj = decomposeColor(color); const colorObj = decomposeColor(color);
coefficient = clamp(coefficient); coefficient = clamp(coefficient);
if (colorObj.type.indexOf('hsl') !== -1) { if (colorObj.type.indexOf("hsl") !== -1) {
colorObj.values[2] *= 1 - coefficient; colorObj.values[2] *= 1 - coefficient;
} else if (colorObj.type.indexOf('rgb') !== -1 || colorObj.type.indexOf('color') !== -1) { } else if (
colorObj.type.indexOf("rgb") !== -1 ||
colorObj.type.indexOf("color") !== -1
) {
for (let i = 0; i < 3; i += 1) { for (let i = 0; i < 3; i += 1) {
colorObj.values[i] *= 1 - coefficient; colorObj.values[i] *= 1 - coefficient;
} }
@ -246,13 +284,13 @@ export function lighten(color: string, coefficient: number): string {
const colorObj = decomposeColor(color); const colorObj = decomposeColor(color);
coefficient = clamp(coefficient); coefficient = clamp(coefficient);
if (colorObj.type.indexOf('hsl') !== -1) { if (colorObj.type.indexOf("hsl") !== -1) {
colorObj.values[2] += (100 - colorObj.values[2]) * coefficient; colorObj.values[2] += (100 - colorObj.values[2]) * coefficient;
} else if (colorObj.type.indexOf('rgb') !== -1) { } else if (colorObj.type.indexOf("rgb") !== -1) {
for (let i = 0; i < 3; i += 1) { for (let i = 0; i < 3; i += 1) {
colorObj.values[i] += (255 - colorObj.values[i]) * coefficient; colorObj.values[i] += (255 - colorObj.values[i]) * coefficient;
} }
} else if (colorObj.type.indexOf('color') !== -1) { } else if (colorObj.type.indexOf("color") !== -1) {
for (let i = 0; i < 3; i += 1) { for (let i = 0; i < 3; i += 1) {
colorObj.values[i] += (1 - colorObj.values[i]) * coefficient; colorObj.values[i] += (1 - colorObj.values[i]) * coefficient;
} }
@ -269,5 +307,7 @@ export function lighten(color: string, coefficient: number): string {
* @returns {string} A CSS color string. Hex input values are returned as rgb * @returns {string} A CSS color string. Hex input values are returned as rgb
*/ */
export function emphasize(color: string, coefficient = 0.15): string { export function emphasize(color: string, coefficient = 0.15): string {
return getLuminance(color) > 0.5 ? darken(color, coefficient) : lighten(color, coefficient); return getLuminance(color) > 0.5
? darken(color, coefficient)
: lighten(color, coefficient);
} }

View File

@ -23,4 +23,3 @@ export interface SelectFieldHandler {
isDirty?: boolean; isDirty?: boolean;
list: Record<string, string>; list: Record<string, string>;
} }

View File

@ -27,7 +27,6 @@ export interface Permissions {
* *
*/ */
origins?: string[] | undefined; origins?: string[] | undefined;
} }
/** /**
@ -38,8 +37,9 @@ export interface CrossBrowserPermissionsApi {
requestHostPermissions(): Promise<boolean>; requestHostPermissions(): Promise<boolean>;
removeHostPermissions(): Promise<boolean>; removeHostPermissions(): Promise<boolean>;
addPermissionsListener(callback: (p: Permissions, lastError?: string) => void): void; addPermissionsListener(
callback: (p: Permissions, lastError?: string) => void,
): void;
} }
export type MessageFromBackend = { export type MessageFromBackend = {
@ -122,7 +122,6 @@ export interface PlatformAPI {
*/ */
getWalletVersion(): WalletVersion; getWalletVersion(): WalletVersion;
/** /**
* Backend API * Backend API
*/ */
@ -134,7 +133,9 @@ export interface PlatformAPI {
/** /**
* Backend API * Backend API
*/ */
registerTalerHeaderListener(onHeader: (tabId: number, url: string) => void): void; registerTalerHeaderListener(
onHeader: (tabId: number, url: string) => void,
): void;
/** /**
* Frontend API * Frontend API
*/ */
@ -169,14 +170,19 @@ export interface PlatformAPI {
* *
* @return response from the backend * @return response from the backend
*/ */
sendMessageToWalletBackground(operation: string, payload: any): Promise<CoreApiResponse>; sendMessageToWalletBackground(
operation: string,
payload: any,
): Promise<CoreApiResponse>;
/** /**
* Used from the frontend to receive notifications about new information * Used from the frontend to receive notifications about new information
* @param listener * @param listener
* @return function to unsubscribe the listener * @return function to unsubscribe the listener
*/ */
listenToWalletBackground(listener: (message: MessageFromBackend) => void): () => void; listenToWalletBackground(
listener: (message: MessageFromBackend) => void,
): () => void;
/** /**
* Use by the wallet backend to receive operations from frontend (popup & wallet) * Use by the wallet backend to receive operations from frontend (popup & wallet)
@ -184,7 +190,13 @@ export interface PlatformAPI {
* *
* @param onNewMessage * @param onNewMessage
*/ */
listenToAllChannels(onNewMessage: (message: any, sender: any, sendResponse: (r: CoreApiResponse) => void) => void): void; listenToAllChannels(
onNewMessage: (
message: any,
sender: any,
sendResponse: (r: CoreApiResponse) => void,
) => void,
): void;
/** /**
* Used by the wallet backend to send notification about new information * Used by the wallet backend to send notification about new information

View File

@ -15,8 +15,18 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/ */
import { classifyTalerUri, CoreApiResponse, Logger, TalerUriType } from "@gnu-taler/taler-util"; import {
import { CrossBrowserPermissionsApi, MessageFromBackend, Permissions, PlatformAPI } from "./api.js"; classifyTalerUri,
CoreApiResponse,
Logger,
TalerUriType,
} from "@gnu-taler/taler-util";
import {
CrossBrowserPermissionsApi,
MessageFromBackend,
Permissions,
PlatformAPI,
} from "./api.js";
const api: PlatformAPI = { const api: PlatformAPI = {
isFirefox, isFirefox,
@ -39,7 +49,7 @@ const api: PlatformAPI = {
useServiceWorkerAsBackgroundProcess, useServiceWorkerAsBackgroundProcess,
containsTalerHeaderListener, containsTalerHeaderListener,
keepAlive, keepAlive,
} };
export default api; export default api;
@ -47,16 +57,15 @@ const logger = new Logger("chrome.ts");
function keepAlive(callback: any): void { function keepAlive(callback: any): void {
if (extensionIsManifestV3()) { if (extensionIsManifestV3()) {
chrome.alarms.create("wallet-worker", { periodInMinutes: 1 }) chrome.alarms.create("wallet-worker", { periodInMinutes: 1 });
chrome.alarms.onAlarm.addListener((a) => { chrome.alarms.onAlarm.addListener((a) => {
logger.trace(`kee p alive alarm: ${a.name}`) logger.trace(`kee p alive alarm: ${a.name}`);
// callback() // callback()
}) });
// } else { // } else {
} }
callback(); callback();
} }
function isFirefox(): boolean { function isFirefox(): boolean {
@ -66,34 +75,35 @@ function isFirefox(): boolean {
const hostPermissions = { const hostPermissions = {
permissions: ["webRequest"], permissions: ["webRequest"],
origins: ["http://*/*", "https://*/*"], origins: ["http://*/*", "https://*/*"],
} };
export function containsHostPermissions(): Promise<boolean> { export function containsHostPermissions(): Promise<boolean> {
return new Promise((res, rej) => { return new Promise((res, rej) => {
chrome.permissions.contains(hostPermissions, (resp) => { chrome.permissions.contains(hostPermissions, (resp) => {
const le = chrome.runtime.lastError?.message const le = chrome.runtime.lastError?.message;
if (le) { if (le) {
rej(le) rej(le);
} }
res(resp) res(resp);
}) });
}) });
} }
export async function requestHostPermissions(): Promise<boolean> { export async function requestHostPermissions(): Promise<boolean> {
return new Promise((res, rej) => { return new Promise((res, rej) => {
chrome.permissions.request(hostPermissions, (resp) => { chrome.permissions.request(hostPermissions, (resp) => {
const le = chrome.runtime.lastError?.message const le = chrome.runtime.lastError?.message;
if (le) { if (le) {
rej(le) rej(le);
} }
res(resp) res(resp);
}) });
}) });
} }
type HeaderListenerFunc = (details: chrome.webRequest.WebResponseHeadersDetails) => void type HeaderListenerFunc = (
details: chrome.webRequest.WebResponseHeadersDetails,
) => void;
let currentHeaderListener: HeaderListenerFunc | undefined = undefined; let currentHeaderListener: HeaderListenerFunc | undefined = undefined;
export function containsTalerHeaderListener(): boolean { export function containsTalerHeaderListener(): boolean {
@ -128,26 +138,31 @@ export async function removeHostPermissions(): Promise<boolean> {
} }
return new Promise((res, rej) => { return new Promise((res, rej) => {
chrome.permissions.remove(hostPermissions, (resp) => { chrome.permissions.remove(hostPermissions, (resp) => {
const le = chrome.runtime.lastError?.message const le = chrome.runtime.lastError?.message;
if (le) { if (le) {
rej(le) rej(le);
} }
res(resp) res(resp);
}) });
}) });
} }
function addPermissionsListener(callback: (p: Permissions, lastError?: string) => void): void { function addPermissionsListener(
callback: (p: Permissions, lastError?: string) => void,
): void {
chrome.permissions.onAdded.addListener((perm: Permissions) => { chrome.permissions.onAdded.addListener((perm: Permissions) => {
const lastError = chrome.runtime.lastError?.message; const lastError = chrome.runtime.lastError?.message;
callback(perm, lastError) callback(perm, lastError);
}) });
} }
function getPermissionsApi(): CrossBrowserPermissionsApi { function getPermissionsApi(): CrossBrowserPermissionsApi {
return { return {
addPermissionsListener, containsHostPermissions, requestHostPermissions, removeHostPermissions addPermissionsListener,
} containsHostPermissions,
requestHostPermissions,
removeHostPermissions,
};
} }
/** /**
@ -156,29 +171,36 @@ function getPermissionsApi(): CrossBrowserPermissionsApi {
*/ */
function notifyWhenAppIsReady(callback: () => void): void { function notifyWhenAppIsReady(callback: () => void): void {
if (extensionIsManifestV3()) { if (extensionIsManifestV3()) {
callback() callback();
} else { } else {
window.addEventListener("load", callback); window.addEventListener("load", callback);
} }
} }
function openWalletURIFromPopup(talerUri: string): void { function openWalletURIFromPopup(talerUri: string): void {
const uriType = classifyTalerUri(talerUri); const uriType = classifyTalerUri(talerUri);
let url: string | undefined = undefined; let url: string | undefined = undefined;
switch (uriType) { switch (uriType) {
case TalerUriType.TalerWithdraw: case TalerUriType.TalerWithdraw:
url = chrome.runtime.getURL(`static/wallet.html#/cta/withdraw?talerWithdrawUri=${talerUri}`); url = chrome.runtime.getURL(
`static/wallet.html#/cta/withdraw?talerWithdrawUri=${talerUri}`,
);
break; break;
case TalerUriType.TalerPay: case TalerUriType.TalerPay:
url = chrome.runtime.getURL(`static/wallet.html#/cta/pay?talerPayUri=${talerUri}`); url = chrome.runtime.getURL(
`static/wallet.html#/cta/pay?talerPayUri=${talerUri}`,
);
break; break;
case TalerUriType.TalerTip: case TalerUriType.TalerTip:
url = chrome.runtime.getURL(`static/wallet.html#/cta/tip?talerTipUri=${talerUri}`); url = chrome.runtime.getURL(
`static/wallet.html#/cta/tip?talerTipUri=${talerUri}`,
);
break; break;
case TalerUriType.TalerRefund: case TalerUriType.TalerRefund:
url = chrome.runtime.getURL(`static/wallet.html#/cta/refund?talerRefundUri=${talerUri}`); url = chrome.runtime.getURL(
`static/wallet.html#/cta/refund?talerRefundUri=${talerUri}`,
);
break; break;
default: default:
logger.warn( logger.warn(
@ -187,56 +209,54 @@ function openWalletURIFromPopup(talerUri: string): void {
return; return;
} }
chrome.tabs.create( chrome.tabs.create({ active: true, url }, () => {
{ active: true, url, }, window.close();
() => { window.close(); }, });
);
} }
function openWalletPage(page: string): void { function openWalletPage(page: string): void {
const url = chrome.runtime.getURL(`/static/wallet.html#${page}`) const url = chrome.runtime.getURL(`/static/wallet.html#${page}`);
chrome.tabs.create( chrome.tabs.create({ active: true, url });
{ active: true, url, },
);
} }
function openWalletPageFromPopup(page: string): void { function openWalletPageFromPopup(page: string): void {
const url = chrome.runtime.getURL(`/static/wallet.html#${page}`) const url = chrome.runtime.getURL(`/static/wallet.html#${page}`);
chrome.tabs.create( chrome.tabs.create({ active: true, url }, () => {
{ active: true, url, }, window.close();
() => { window.close(); }, });
);
} }
async function sendMessageToWalletBackground(operation: string, payload: any): Promise<any> { async function sendMessageToWalletBackground(
operation: string,
payload: any,
): Promise<any> {
return new Promise<any>((resolve, reject) => { return new Promise<any>((resolve, reject) => {
logger.trace("send operation to the wallet background", operation) logger.trace("send operation to the wallet background", operation);
chrome.runtime.sendMessage({ operation, payload, id: "(none)" }, (resp) => { chrome.runtime.sendMessage({ operation, payload, id: "(none)" }, (resp) => {
if (chrome.runtime.lastError) { if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError.message) reject(chrome.runtime.lastError.message);
} }
resolve(resp) resolve(resp);
// return true to keep the channel open // return true to keep the channel open
return true; return true;
}) });
}) });
} }
let notificationPort: chrome.runtime.Port | undefined; let notificationPort: chrome.runtime.Port | undefined;
function listenToWalletBackground(listener: (m: any) => void): () => void { function listenToWalletBackground(listener: (m: any) => void): () => void {
if (notificationPort === undefined) { if (notificationPort === undefined) {
notificationPort = chrome.runtime.connect({ name: "notifications" }) notificationPort = chrome.runtime.connect({ name: "notifications" });
} }
notificationPort.onMessage.addListener(listener) notificationPort.onMessage.addListener(listener);
function removeListener(): void { function removeListener(): void {
if (notificationPort !== undefined) { if (notificationPort !== undefined) {
notificationPort.onMessage.removeListener(listener) notificationPort.onMessage.removeListener(listener);
} }
} }
return removeListener return removeListener;
} }
const allPorts: chrome.runtime.Port[] = []; const allPorts: chrome.runtime.Port[] = [];
function sendMessageToAllChannels(message: MessageFromBackend): void { function sendMessageToAllChannels(message: MessageFromBackend): void {
@ -262,9 +282,15 @@ function registerAllIncomingConnections(): void {
}); });
} }
function listenToAllChannels(cb: (message: any, sender: any, callback: (r: CoreApiResponse) => void) => void): void { function listenToAllChannels(
cb: (
message: any,
sender: any,
callback: (r: CoreApiResponse) => void,
) => void,
): void {
chrome.runtime.onMessage.addListener((m, s, c) => { chrome.runtime.onMessage.addListener((m, s, c) => {
cb(m, s, c) cb(m, s, c);
// keep the connection open // keep the connection open
return true; return true;
@ -278,13 +304,9 @@ function registerReloadOnNewVersion(): void {
logger.info("update available:", details); logger.info("update available:", details);
chrome.runtime.reload(); chrome.runtime.reload();
}); });
} }
function redirectTabToWalletPage( function redirectTabToWalletPage(tabId: number, page: string): void {
tabId: number,
page: string,
): void {
const url = chrome.runtime.getURL(`/static/wallet.html#${page}`); const url = chrome.runtime.getURL(`/static/wallet.html#${page}`);
logger.trace("redirecting tabId: ", tabId, " to: ", url); logger.trace("redirecting tabId: ", tabId, " to: ", url);
chrome.tabs.update(tabId, { url }); chrome.tabs.update(tabId, { url });
@ -300,7 +322,9 @@ function getWalletVersion(): WalletVersion {
return manifestData; return manifestData;
} }
function registerTalerHeaderListener(callback: (tabId: number, url: string) => void): void { function registerTalerHeaderListener(
callback: (tabId: number, url: string) => void,
): void {
logger.trace("setting up header listener"); logger.trace("setting up header listener");
function headerListener( function headerListener(
@ -316,18 +340,20 @@ function registerTalerHeaderListener(callback: (tabId: number, url: string) => v
details.statusCode === 200 details.statusCode === 200
) { ) {
const values = (details.responseHeaders || []) const values = (details.responseHeaders || [])
.filter(h => h.name.toLowerCase() === 'taler') .filter((h) => h.name.toLowerCase() === "taler")
.map(h => h.value) .map((h) => h.value)
.filter((value): value is string => !!value) .filter((value): value is string => !!value);
if (values.length > 0) { if (values.length > 0) {
callback(details.tabId, values[0]) callback(details.tabId, values[0]);
} }
} }
return; return;
} }
const prevHeaderListener = currentHeaderListener; const prevHeaderListener = currentHeaderListener;
getPermissionsApi().containsHostPermissions().then(result => { getPermissionsApi()
.containsHostPermissions()
.then((result) => {
//if there is a handler already, remove it //if there is a handler already, remove it
if ( if (
prevHeaderListener && prevHeaderListener &&
@ -337,13 +363,12 @@ function registerTalerHeaderListener(callback: (tabId: number, url: string) => v
} }
//if the result was positive, add the headerListener //if the result was positive, add the headerListener
if (result) { if (result) {
const listener: chrome.webRequest.WebResponseHeadersEvent | undefined = chrome?.webRequest?.onHeadersReceived; const listener: chrome.webRequest.WebResponseHeadersEvent | undefined =
chrome?.webRequest?.onHeadersReceived;
if (listener) { if (listener) {
listener.addListener( listener.addListener(headerListener, { urls: ["<all_urls>"] }, [
headerListener, "responseHeaders",
{ urls: ["<all_urls>"] }, ]);
["responseHeaders"],
);
currentHeaderListener = headerListener; currentHeaderListener = headerListener;
} }
} }
@ -365,8 +390,8 @@ const alertIcons = {
"64": "/static/img/taler-alert-64.png", "64": "/static/img/taler-alert-64.png",
"128": "/static/img/taler-alert-128.png", "128": "/static/img/taler-alert-128.png",
"256": "/static/img/taler-alert-256.png", "256": "/static/img/taler-alert-256.png",
"512": "/static/img/taler-alert-512.png" "512": "/static/img/taler-alert-512.png",
} };
const normalIcons = { const normalIcons = {
"16": "/static/img/taler-logo-16.png", "16": "/static/img/taler-logo-16.png",
"19": "/static/img/taler-logo-19.png", "19": "/static/img/taler-logo-19.png",
@ -376,70 +401,99 @@ const normalIcons = {
"64": "/static/img/taler-logo-64.png", "64": "/static/img/taler-logo-64.png",
"128": "/static/img/taler-logo-128.png", "128": "/static/img/taler-logo-128.png",
"256": "/static/img/taler-logo-256.png", "256": "/static/img/taler-logo-256.png",
"512": "/static/img/taler-logo-512.png" "512": "/static/img/taler-logo-512.png",
} };
function setNormalIcon(): void { function setNormalIcon(): void {
if (extensionIsManifestV3()) { if (extensionIsManifestV3()) {
chrome.action.setIcon({ path: normalIcons }) chrome.action.setIcon({ path: normalIcons });
} else { } else {
chrome.browserAction.setIcon({ path: normalIcons }) chrome.browserAction.setIcon({ path: normalIcons });
} }
} }
function setAlertedIcon(): void { function setAlertedIcon(): void {
if (extensionIsManifestV3()) { if (extensionIsManifestV3()) {
chrome.action.setIcon({ path: alertIcons }) chrome.action.setIcon({ path: alertIcons });
} else { } else {
chrome.browserAction.setIcon({ path: alertIcons }) chrome.browserAction.setIcon({ path: alertIcons });
} }
} }
interface OffscreenCanvasRenderingContext2D
interface OffscreenCanvasRenderingContext2D extends CanvasState, CanvasTransform, CanvasCompositing, CanvasImageSmoothing, CanvasFillStrokeStyles, CanvasShadowStyles, CanvasFilters, CanvasRect, CanvasDrawPath, CanvasUserInterface, CanvasText, CanvasDrawImage, CanvasImageData, CanvasPathDrawingStyles, CanvasTextDrawingStyles, CanvasPath { extends CanvasState,
CanvasTransform,
CanvasCompositing,
CanvasImageSmoothing,
CanvasFillStrokeStyles,
CanvasShadowStyles,
CanvasFilters,
CanvasRect,
CanvasDrawPath,
CanvasUserInterface,
CanvasText,
CanvasDrawImage,
CanvasImageData,
CanvasPathDrawingStyles,
CanvasTextDrawingStyles,
CanvasPath {
readonly canvas: OffscreenCanvas; readonly canvas: OffscreenCanvas;
} }
declare const OffscreenCanvasRenderingContext2D: { declare const OffscreenCanvasRenderingContext2D: {
prototype: OffscreenCanvasRenderingContext2D; prototype: OffscreenCanvasRenderingContext2D;
new(): OffscreenCanvasRenderingContext2D; new (): OffscreenCanvasRenderingContext2D;
} };
interface OffscreenCanvas extends EventTarget { interface OffscreenCanvas extends EventTarget {
width: number; width: number;
height: number; height: number;
getContext(contextId: "2d", contextAttributes?: CanvasRenderingContext2DSettings): OffscreenCanvasRenderingContext2D | null; getContext(
contextId: "2d",
contextAttributes?: CanvasRenderingContext2DSettings,
): OffscreenCanvasRenderingContext2D | null;
} }
declare const OffscreenCanvas: { declare const OffscreenCanvas: {
prototype: OffscreenCanvas; prototype: OffscreenCanvas;
new(width: number, height: number): OffscreenCanvas; new (width: number, height: number): OffscreenCanvas;
} };
function createCanvas(size: number): OffscreenCanvas { function createCanvas(size: number): OffscreenCanvas {
if (extensionIsManifestV3()) { if (extensionIsManifestV3()) {
return new OffscreenCanvas(size, size) return new OffscreenCanvas(size, size);
} else { } else {
const c = document.createElement("canvas") const c = document.createElement("canvas");
c.height = size; c.height = size;
c.width = size; c.width = size;
return c; return c;
} }
} }
async function createImage(size: number, file: string): Promise<ImageData> { async function createImage(size: number, file: string): Promise<ImageData> {
const r = await fetch(file) const r = await fetch(file);
const b = await r.blob() const b = await r.blob();
const image = await createImageBitmap(b) const image = await createImageBitmap(b);
const canvas = createCanvas(size); const canvas = createCanvas(size);
const canvasContext = canvas.getContext('2d')!; const canvasContext = canvas.getContext("2d")!;
canvasContext.clearRect(0, 0, canvas.width, canvas.height); canvasContext.clearRect(0, 0, canvas.width, canvas.height);
canvasContext.drawImage(image, 0, 0, canvas.width, canvas.height); canvasContext.drawImage(image, 0, 0, canvas.width, canvas.height);
const imageData = canvasContext.getImageData(0, 0, canvas.width, canvas.height); const imageData = canvasContext.getImageData(
0,
0,
canvas.width,
canvas.height,
);
return imageData; return imageData;
} }
async function registerIconChangeOnTalerContent(): Promise<void> { async function registerIconChangeOnTalerContent(): Promise<void> {
const imgs = await Promise.all(Object.entries(alertIcons).map(([key, value]) => createImage(parseInt(key, 10), value))) const imgs = await Promise.all(
const imageData = imgs.reduce((prev, cur) => ({ ...prev, [cur.width]: cur }), {} as { [size: string]: ImageData }) Object.entries(alertIcons).map(([key, value]) =>
createImage(parseInt(key, 10), value),
),
);
const imageData = imgs.reduce(
(prev, cur) => ({ ...prev, [cur.width]: cur }),
{} as { [size: string]: ImageData },
);
if (chrome.declarativeContent) { if (chrome.declarativeContent) {
// using declarative content does not need host permission // using declarative content does not need host permission
@ -447,49 +501,54 @@ async function registerIconChangeOnTalerContent(): Promise<void> {
const secureTalerUrlLookup = { const secureTalerUrlLookup = {
conditions: [ conditions: [
new chrome.declarativeContent.PageStateMatcher({ new chrome.declarativeContent.PageStateMatcher({
css: ["a[href^='taler://'"] css: ["a[href^='taler://'"],
}) }),
], ],
actions: [new chrome.declarativeContent.SetIcon({ imageData })] actions: [new chrome.declarativeContent.SetIcon({ imageData })],
}; };
const inSecureTalerUrlLookup = { const inSecureTalerUrlLookup = {
conditions: [ conditions: [
new chrome.declarativeContent.PageStateMatcher({ new chrome.declarativeContent.PageStateMatcher({
css: ["a[href^='taler+http://'"] css: ["a[href^='taler+http://'"],
}) }),
], ],
actions: [new chrome.declarativeContent.SetIcon({ imageData })] actions: [new chrome.declarativeContent.SetIcon({ imageData })],
}; };
chrome.declarativeContent.onPageChanged.removeRules(undefined, function () { chrome.declarativeContent.onPageChanged.removeRules(undefined, function () {
chrome.declarativeContent.onPageChanged.addRules([secureTalerUrlLookup, inSecureTalerUrlLookup]); chrome.declarativeContent.onPageChanged.addRules([
secureTalerUrlLookup,
inSecureTalerUrlLookup,
]);
}); });
return; return;
} }
//this browser doesn't have declarativeContent //this browser doesn't have declarativeContent
//we need host_permission and we will check the content for changing the icon //we need host_permission and we will check the content for changing the icon
chrome.tabs.onUpdated.addListener(async (tabId, info: chrome.tabs.TabChangeInfo) => { chrome.tabs.onUpdated.addListener(
async (tabId, info: chrome.tabs.TabChangeInfo) => {
if (tabId < 0) return; if (tabId < 0) return;
logger.info("tab updated", tabId, info); logger.info("tab updated", tabId, info);
if (info.status !== "complete") return; if (info.status !== "complete") return;
const uri = await findTalerUriInTab(tabId); const uri = await findTalerUriInTab(tabId);
if (uri) { if (uri) {
setAlertedIcon() setAlertedIcon();
} else { } else {
setNormalIcon() setNormalIcon();
} }
},
}); );
chrome.tabs.onActivated.addListener(async ({ tabId }: chrome.tabs.TabActiveInfo) => { chrome.tabs.onActivated.addListener(
async ({ tabId }: chrome.tabs.TabActiveInfo) => {
if (tabId < 0) return; if (tabId < 0) return;
const uri = await findTalerUriInTab(tabId); const uri = await findTalerUriInTab(tabId);
if (uri) { if (uri) {
setAlertedIcon() setAlertedIcon();
} else { } else {
setNormalIcon() setNormalIcon();
} }
}) },
);
} }
function registerOnInstalled(callback: () => void): void { function registerOnInstalled(callback: () => void): void {
@ -498,27 +557,27 @@ function registerOnInstalled(callback: () => void): void {
chrome.runtime.onInstalled.addListener(async (details) => { chrome.runtime.onInstalled.addListener(async (details) => {
logger.info(`onInstalled with reason: "${details.reason}"`); logger.info(`onInstalled with reason: "${details.reason}"`);
if (details.reason === chrome.runtime.OnInstalledReason.INSTALL) { if (details.reason === chrome.runtime.OnInstalledReason.INSTALL) {
callback() callback();
} }
registerIconChangeOnTalerContent() registerIconChangeOnTalerContent();
}); });
} }
function extensionIsManifestV3(): boolean { function extensionIsManifestV3(): boolean {
return chrome.runtime.getManifest().manifest_version === 3 return chrome.runtime.getManifest().manifest_version === 3;
} }
function useServiceWorkerAsBackgroundProcess(): boolean { function useServiceWorkerAsBackgroundProcess(): boolean {
return extensionIsManifestV3() return extensionIsManifestV3();
} }
function searchForTalerLinks(): string | undefined { function searchForTalerLinks(): string | undefined {
let found; let found;
found = document.querySelector("a[href^='taler://'") found = document.querySelector("a[href^='taler://'");
if (found) return found.toString() if (found) return found.toString();
found = document.querySelector("a[href^='taler+http://'") found = document.querySelector("a[href^='taler+http://'");
if (found) return found.toString() if (found) return found.toString();
return undefined return undefined;
} }
async function getCurrentTab(): Promise<chrome.tabs.Tab> { async function getCurrentTab(): Promise<chrome.tabs.Tab> {
@ -526,12 +585,12 @@ async function getCurrentTab(): Promise<chrome.tabs.Tab> {
return new Promise<chrome.tabs.Tab>((resolve, reject) => { return new Promise<chrome.tabs.Tab>((resolve, reject) => {
chrome.tabs.query(queryOptions, (tabs) => { chrome.tabs.query(queryOptions, (tabs) => {
if (chrome.runtime.lastError) { if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError) reject(chrome.runtime.lastError);
return; return;
} }
resolve(tabs[0]) resolve(tabs[0]);
});
}); });
})
} }
async function findTalerUriInTab(tabId: number): Promise<string | undefined> { async function findTalerUriInTab(tabId: number): Promise<string | undefined> {
@ -541,16 +600,17 @@ async function findTalerUriInTab(tabId: number): Promise<string | undefined> {
const res = await chrome.scripting.executeScript({ const res = await chrome.scripting.executeScript({
target: { tabId, allFrames: true }, target: { tabId, allFrames: true },
func: searchForTalerLinks, func: searchForTalerLinks,
args: [] args: [],
}) });
return res[0].result return res[0].result;
} catch (e) { } catch (e) {
return; return;
} }
} else { } else {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
//manifest v2 //manifest v2
chrome.tabs.executeScript(tabId, chrome.tabs.executeScript(
tabId,
{ {
code: ` code: `
(() => { (() => {
@ -576,6 +636,5 @@ async function findTalerUriInTab(tabId: number): Promise<string | undefined> {
async function findTalerUriInActiveTab(): Promise<string | undefined> { async function findTalerUriInActiveTab(): Promise<string | undefined> {
const tab = await getCurrentTab(); const tab = await getCurrentTab();
if (!tab || tab.id === undefined) return; if (!tab || tab.id === undefined) return;
return findTalerUriInTab(tab.id) return findTalerUriInTab(tab.id);
} }

View File

@ -17,50 +17,55 @@
import { CoreApiResponse } from "@gnu-taler/taler-util"; import { CoreApiResponse } from "@gnu-taler/taler-util";
import { MessageFromBackend, PlatformAPI } from "./api.js"; import { MessageFromBackend, PlatformAPI } from "./api.js";
const frames = ["popup", "wallet"] const frames = ["popup", "wallet"];
const api: PlatformAPI = ({ const api: PlatformAPI = {
isFirefox: () => false, isFirefox: () => false,
keepAlive: (cb: VoidFunction) => cb(), keepAlive: (cb: VoidFunction) => cb(),
findTalerUriInActiveTab: async () => undefined, findTalerUriInActiveTab: async () => undefined,
containsTalerHeaderListener: () => { return true }, containsTalerHeaderListener: () => {
return true;
},
getPermissionsApi: () => ({ getPermissionsApi: () => ({
addPermissionsListener: () => undefined, containsHostPermissions: async () => true, removeHostPermissions: async () => false, requestHostPermissions: async () => false addPermissionsListener: () => undefined,
containsHostPermissions: async () => true,
removeHostPermissions: async () => false,
requestHostPermissions: async () => false,
}), }),
getWalletVersion: () => ({ getWalletVersion: () => ({
version: 'none' version: "none",
}), }),
notifyWhenAppIsReady: (fn: () => void) => { notifyWhenAppIsReady: (fn: () => void) => {
let total = frames.length let total = frames.length;
function waitAndNotify(): void { function waitAndNotify(): void {
total-- total--;
if (total < 1) { if (total < 1) {
console.log('done') console.log("done");
fn() fn();
} }
} }
frames.forEach(f => { frames.forEach((f) => {
const theFrame = window.frames[f as any] const theFrame = window.frames[f as any];
if (theFrame.location.href === 'about:blank') { if (theFrame.location.href === "about:blank") {
waitAndNotify() waitAndNotify();
} else { } else {
theFrame.addEventListener("load", waitAndNotify) theFrame.addEventListener("load", waitAndNotify);
} }
}) });
}, },
openWalletPage: (page: string) => { openWalletPage: (page: string) => {
window.frames['wallet' as any].location = `/wallet.html#${page}` window.frames["wallet" as any].location = `/wallet.html#${page}`;
}, },
openWalletPageFromPopup: (page: string) => { openWalletPageFromPopup: (page: string) => {
window.parent.frames['wallet' as any].location = `/wallet.html#${page}` window.parent.frames["wallet" as any].location = `/wallet.html#${page}`;
window.location.href = "about:blank" window.location.href = "about:blank";
}, },
openWalletURIFromPopup: (page: string) => { openWalletURIFromPopup: (page: string) => {
alert('openWalletURIFromPopup not implemented yet') alert("openWalletURIFromPopup not implemented yet");
}, },
redirectTabToWalletPage: (tabId: number, page: string) => { redirectTabToWalletPage: (tabId: number, page: string) => {
alert('redirectTabToWalletPage not implemented yet') alert("redirectTabToWalletPage not implemented yet");
}, },
registerAllIncomingConnections: () => undefined, registerAllIncomingConnections: () => undefined,
@ -70,91 +75,101 @@ const api: PlatformAPI = ({
useServiceWorkerAsBackgroundProcess: () => false, useServiceWorkerAsBackgroundProcess: () => false,
listenToAllChannels: (fn: (m: any, s: any, c: (r: CoreApiResponse) => void) => void) => { listenToAllChannels: (
window.addEventListener("message", (event: MessageEvent<IframeMessageType>) => { fn: (m: any, s: any, c: (r: CoreApiResponse) => void) => void,
if (event.data.type !== 'command') return ) => {
const sender = event.data.header.replyMe window.addEventListener(
"message",
(event: MessageEvent<IframeMessageType>) => {
if (event.data.type !== "command") return;
const sender = event.data.header.replyMe;
fn(event.data.body, sender, (resp: CoreApiResponse) => { fn(event.data.body, sender, (resp: CoreApiResponse) => {
if (event.source) { if (event.source) {
const msg: IframeMessageResponse = { const msg: IframeMessageResponse = {
type: "response", type: "response",
header: { responseId: sender }, header: { responseId: sender },
body: resp body: resp,
};
window.parent.postMessage(msg);
} }
window.parent.postMessage(msg) });
} },
}) );
})
}, },
sendMessageToAllChannels: (message: MessageFromBackend) => { sendMessageToAllChannels: (message: MessageFromBackend) => {
Array.from(window.frames).forEach(w => { Array.from(window.frames).forEach((w) => {
try { try {
w.postMessage({ w.postMessage({
header: {}, body: message header: {},
body: message,
}); });
} catch (e) { } catch (e) {
console.error(e) console.error(e);
} }
}) });
}, },
listenToWalletBackground: (onNewMessage: (m: MessageFromBackend) => void) => { listenToWalletBackground: (onNewMessage: (m: MessageFromBackend) => void) => {
function listener(event: MessageEvent<IframeMessageType>): void { function listener(event: MessageEvent<IframeMessageType>): void {
if (event.data.type !== 'notification') return if (event.data.type !== "notification") return;
onNewMessage(event.data.body) onNewMessage(event.data.body);
} }
window.parent.addEventListener("message", listener) window.parent.addEventListener("message", listener);
return () => { return () => {
window.parent.removeEventListener("message", listener) window.parent.removeEventListener("message", listener);
} };
}, },
sendMessageToWalletBackground: async (operation: string, payload: any) => { sendMessageToWalletBackground: async (operation: string, payload: any) => {
const replyMe = `reply-${Math.floor(Math.random() * 100000)}` const replyMe = `reply-${Math.floor(Math.random() * 100000)}`;
const message: IframeMessageCommand = { const message: IframeMessageCommand = {
type: 'command', type: "command",
header: { replyMe }, header: { replyMe },
body: { operation, payload, id: "(none)" } body: { operation, payload, id: "(none)" },
} };
window.parent.postMessage(message) window.parent.postMessage(message);
return new Promise((res, rej) => { return new Promise((res, rej) => {
function listener(event: MessageEvent<IframeMessageType>): void { function listener(event: MessageEvent<IframeMessageType>): void {
if (event.data.type !== "response" || event.data.header.responseId !== replyMe) { if (
return event.data.type !== "response" ||
event.data.header.responseId !== replyMe
) {
return;
} }
res(event.data.body) res(event.data.body);
window.parent.removeEventListener("message", listener) window.parent.removeEventListener("message", listener);
} }
window.parent.addEventListener("message", listener, { window.parent.addEventListener("message", listener, {});
});
})
})
}, },
}) };
type IframeMessageType = IframeMessageNotification | IframeMessageResponse | IframeMessageCommand; type IframeMessageType =
| IframeMessageNotification
| IframeMessageResponse
| IframeMessageCommand;
interface IframeMessageNotification { interface IframeMessageNotification {
type: "notification"; type: "notification";
header: Record<string, never>, header: Record<string, never>;
body: MessageFromBackend body: MessageFromBackend;
} }
interface IframeMessageResponse { interface IframeMessageResponse {
type: "response"; type: "response";
header: { header: {
responseId: string; responseId: string;
}, };
body: CoreApiResponse body: CoreApiResponse;
} }
interface IframeMessageCommand { interface IframeMessageCommand {
type: "command"; type: "command";
header: { header: {
replyMe: string; replyMe: string;
}, };
body: { body: {
operation: any, id: string, payload: any operation: any;
} id: string;
payload: any;
};
} }
export default api; export default api;

View File

@ -15,7 +15,11 @@
*/ */
import { CrossBrowserPermissionsApi, Permissions, PlatformAPI } from "./api.js"; import { CrossBrowserPermissionsApi, Permissions, PlatformAPI } from "./api.js";
import chromePlatform, { containsHostPermissions as chromeContains, removeHostPermissions as chromeRemove, requestHostPermissions as chromeRequest } from "./chrome.js"; import chromePlatform, {
containsHostPermissions as chromeContains,
removeHostPermissions as chromeRemove,
requestHostPermissions as chromeRequest,
} from "./chrome.js";
const api: PlatformAPI = { const api: PlatformAPI = {
...chromePlatform, ...chromePlatform,
@ -23,18 +27,17 @@ const api: PlatformAPI = {
getPermissionsApi, getPermissionsApi,
notifyWhenAppIsReady, notifyWhenAppIsReady,
redirectTabToWalletPage, redirectTabToWalletPage,
useServiceWorkerAsBackgroundProcess useServiceWorkerAsBackgroundProcess,
}; };
export default api; export default api;
function isFirefox(): boolean { function isFirefox(): boolean {
return true return true;
} }
function addPermissionsListener(callback: (p: Permissions) => void): void { function addPermissionsListener(callback: (p: Permissions) => void): void {
console.log("addPermissionListener is not supported for Firefox") console.log("addPermissionListener is not supported for Firefox");
} }
function getPermissionsApi(): CrossBrowserPermissionsApi { function getPermissionsApi(): CrossBrowserPermissionsApi {
@ -42,8 +45,8 @@ function getPermissionsApi(): CrossBrowserPermissionsApi {
addPermissionsListener, addPermissionsListener,
containsHostPermissions: chromeContains, containsHostPermissions: chromeContains,
requestHostPermissions: chromeRequest, requestHostPermissions: chromeRequest,
removeHostPermissions: chromeRemove removeHostPermissions: chromeRemove,
} };
} }
/** /**
@ -52,23 +55,18 @@ function getPermissionsApi(): CrossBrowserPermissionsApi {
*/ */
function notifyWhenAppIsReady(callback: () => void): void { function notifyWhenAppIsReady(callback: () => void): void {
if (chrome.runtime && chrome.runtime.getManifest().manifest_version === 3) { if (chrome.runtime && chrome.runtime.getManifest().manifest_version === 3) {
callback() callback();
} else { } else {
window.addEventListener("load", callback); window.addEventListener("load", callback);
} }
} }
function redirectTabToWalletPage(tabId: number, page: string): void {
function redirectTabToWalletPage(
tabId: number,
page: string,
): void {
const url = chrome.runtime.getURL(`/static/wallet.html#${page}`); const url = chrome.runtime.getURL(`/static/wallet.html#${page}`);
console.log("redirecting tabId: ", tabId, " to: ", url); console.log("redirecting tabId: ", tabId, " to: ", url);
chrome.tabs.update(tabId, { url, loadReplace: true } as any); chrome.tabs.update(tabId, { url, loadReplace: true } as any);
} }
function useServiceWorkerAsBackgroundProcess(): false { function useServiceWorkerAsBackgroundProcess(): false {
return false return false;
} }

View File

@ -93,7 +93,7 @@ export class ServiceWorkerHttpLib implements HttpRequestLibrary {
): Promise<HttpResponse> { ): Promise<HttpResponse> {
return this.fetch(url, { return this.fetch(url, {
method: "POST", method: "POST",
headers: {"Content-Type": "application/json"}, headers: { "Content-Type": "application/json" },
body: JSON.stringify(body), body: JSON.stringify(body),
...opt, ...opt,
}); });

View File

@ -47,9 +47,9 @@ function testThisStory(st: any): any {
describe("render every storybook example", () => { describe("render every storybook example", () => {
[popup, wallet, cta, mui, components].forEach(function testAll(st: any) { [popup, wallet, cta, mui, components].forEach(function testAll(st: any) {
if (Array.isArray(st.default)) { if (Array.isArray(st.default)) {
st.default.forEach(testAll) st.default.forEach(testAll);
} else { } else {
testThisStory(st) testThisStory(st);
} }
}); });
}); });

View File

@ -14,15 +14,23 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/ */
import { ComponentChildren, Fragment, FunctionalComponent, h as create, options, render as renderIntoDom, VNode } from "preact"; import {
ComponentChildren,
Fragment,
FunctionalComponent,
h as create,
options,
render as renderIntoDom,
VNode,
} from "preact";
import { render as renderToString } from "preact-render-to-string"; import { render as renderToString } from "preact-render-to-string";
// When doing tests we want the requestAnimationFrame to be as fast as possible. // When doing tests we want the requestAnimationFrame to be as fast as possible.
// without this option the RAF will timeout after 100ms making the tests slower // without this option the RAF will timeout after 100ms making the tests slower
options.requestAnimationFrame = (fn: () => void) => { options.requestAnimationFrame = (fn: () => void) => {
// console.log("RAF called") // console.log("RAF called")
return fn() return fn();
} };
export function createExample<Props>( export function createExample<Props>(
Component: FunctionalComponent<Props>, Component: FunctionalComponent<Props>,
@ -31,7 +39,7 @@ export function createExample<Props>(
//FIXME: props are evaluated on build time //FIXME: props are evaluated on build time
// in some cases we want to evaluated the props on render time so we can get some relative timestamp // in some cases we want to evaluated the props on render time so we can get some relative timestamp
// 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 Render;
@ -43,14 +51,22 @@ export function createExampleWithCustomContext<Props, ContextProps>(
ContextProvider: FunctionalComponent<ContextProps>, ContextProvider: FunctionalComponent<ContextProps>,
contextProps: Partial<ContextProps>, contextProps: Partial<ContextProps>,
): ComponentChildren { ): ComponentChildren {
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);
const WithContext = (args: any): VNode => create(ContextProvider, { ...contextProps, children: [Render(args)] } as any); const WithContext = (args: any): VNode =>
WithContext.args = evaluatedProps create(ContextProvider, {
return WithContext ...contextProps,
children: [Render(args)],
} as any);
WithContext.args = evaluatedProps;
return WithContext;
} }
export function NullLink({ children }: { children?: ComponentChildren }): VNode { export function NullLink({
children,
}: {
children?: ComponentChildren;
}): VNode {
return create("a", { children, href: "javascript:void(0);" }); return create("a", { children, href: "javascript:void(0);" });
} }
@ -74,53 +90,59 @@ interface Mounted<T> {
waitNextUpdate: (s?: string) => Promise<void>; waitNextUpdate: (s?: string) => Promise<void>;
} }
const isNode = typeof window === "undefined" const isNode = typeof window === "undefined";
export function mountHook<T>(callback: () => T, Context?: ({ children }: { children: any }) => VNode): Mounted<T> { export function mountHook<T>(
callback: () => T,
Context?: ({ children }: { children: any }) => VNode,
): Mounted<T> {
// const result: { current: T | null } = { // const result: { current: T | null } = {
// current: null // current: null
// } // }
let lastResult: T | Error | null = null; let lastResult: T | Error | null = null;
const listener: Array<() => void> = [] const listener: Array<() => void> = [];
// component that's going to hold the hook // component that's going to hold the hook
function Component(): VNode { function Component(): VNode {
try { try {
lastResult = callback() lastResult = callback();
} catch (e) { } catch (e) {
if (e instanceof Error) { if (e instanceof Error) {
lastResult = e lastResult = e;
} else { } else {
lastResult = new Error(`mounting the hook throw an exception: ${e}`) lastResult = new Error(`mounting the hook throw an exception: ${e}`);
} }
} }
// notify to everyone waiting for an update and clean the queue // notify to everyone waiting for an update and clean the queue
listener.splice(0, listener.length).forEach(cb => cb()) listener.splice(0, listener.length).forEach((cb) => cb());
return create(Fragment, {}) return create(Fragment, {});
} }
// create the vdom with context if required // create the vdom with context if required
const vdom = !Context ? create(Component, {}) : create(Context, { children: [create(Component, {})] },); const vdom = !Context
? create(Component, {})
: create(Context, { children: [create(Component, {})] });
// waiter callback // waiter callback
async function waitNextUpdate(_label = ""): Promise<void> { async function waitNextUpdate(_label = ""): Promise<void> {
if (_label) _label = `. label: "${_label}"` if (_label) _label = `. label: "${_label}"`;
await new Promise((res, rej) => { await new Promise((res, rej) => {
const tid = setTimeout(() => { const tid = setTimeout(() => {
rej(Error(`waiting for an update but the hook didn't make one${_label}`)) rej(
}, 100) Error(`waiting for an update but the hook didn't make one${_label}`),
);
}, 100);
listener.push(() => { listener.push(() => {
clearTimeout(tid) clearTimeout(tid);
res(undefined) res(undefined);
}) });
}) });
} }
const customElement = {} as Element const customElement = {} as Element;
const parentElement = isNode ? customElement : document.createElement("div"); const parentElement = isNode ? customElement : document.createElement("div");
if (!isNode) { if (!isNode) {
document.body.appendChild(parentElement); document.body.appendChild(parentElement);
@ -136,38 +158,44 @@ export function mountHook<T>(callback: () => T, Context?: ({ children }: { child
} }
function getLastResult(): T | Error | null { function getLastResult(): T | Error | null {
const copy = lastResult const copy = lastResult;
lastResult = null lastResult = null;
return copy; return copy;
} }
function getLastResultOrThrow(): T { function getLastResultOrThrow(): T {
const r = getLastResult() const r = getLastResult();
if (r instanceof Error) throw r; if (r instanceof Error) throw r;
if (!r) throw Error('there was no last result') if (!r) throw Error("there was no last result");
return r; return r;
} }
async function assertNoPendingUpdate(): Promise<void> { async function assertNoPendingUpdate(): Promise<void> {
await new Promise((res, rej) => { await new Promise((res, rej) => {
const tid = setTimeout(() => { const tid = setTimeout(() => {
res(undefined) res(undefined);
}, 10) }, 10);
listener.push(() => { listener.push(() => {
clearTimeout(tid) clearTimeout(tid);
rej(Error(`Expecting no pending result but the hook got updated. rej(
Error(`Expecting no pending result but the hook got updated.
If the update was not intended you need to check the hook dependencies If the update was not intended you need to check the hook dependencies
(or dependencies of the internal state) but otherwise make (or dependencies of the internal state) but otherwise make
sure to consume the result before ending the test.`)) sure to consume the result before ending the test.`),
}) );
}) });
});
const r = getLastResult() const r = getLastResult();
if (r) throw Error(`There are still pending results. if (r)
throw Error(`There are still pending results.
This may happen because the hook did a new update but the test didn't consume the result using getLastResult`); This may happen because the hook did a new update but the test didn't consume the result using getLastResult`);
} }
return { return {
unmount, getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate unmount,
} getLastResultOrThrow,
waitNextUpdate,
assertNoPendingUpdate,
};
} }

View File

@ -14,8 +14,11 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/ */
import { AmountJson, Amounts, GetExchangeTosResult } from "@gnu-taler/taler-util"; import {
AmountJson,
Amounts,
GetExchangeTosResult,
} from "@gnu-taler/taler-util";
function getJsonIfOk(r: Response): Promise<any> { function getJsonIfOk(r: Response): Promise<any> {
if (r.ok) { if (r.ok) {
@ -27,15 +30,13 @@ function getJsonIfOk(r: Response): Promise<any> {
} }
throw new Error( throw new Error(
`Try another server: (${r.status}) ${r.statusText || "internal server error" `Try another server: (${r.status}) ${
r.statusText || "internal server error"
}`, }`,
); );
} }
export async function queryToSlashConfig<T>( export async function queryToSlashConfig<T>(url: string): Promise<T> {
url: string,
): Promise<T> {
return fetch(new URL("config", url).href) return fetch(new URL("config", url).href)
.catch(() => { .catch(() => {
throw new Error(`Network error`); throw new Error(`Network error`);
@ -46,25 +47,27 @@ export async function queryToSlashConfig<T>(
function timeout<T>(ms: number, promise: Promise<T>): Promise<T> { function timeout<T>(ms: number, promise: Promise<T>): Promise<T> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const timer = setTimeout(() => { const timer = setTimeout(() => {
reject(new Error(`Timeout: the query took longer than ${Math.floor(ms / 1000)} secs`)) reject(
}, ms) new Error(
`Timeout: the query took longer than ${Math.floor(ms / 1000)} secs`,
),
);
}, ms);
promise promise
.then(value => { .then((value) => {
clearTimeout(timer) clearTimeout(timer);
resolve(value) resolve(value);
})
.catch(reason => {
clearTimeout(timer)
reject(reason)
})
}) })
.catch((reason) => {
clearTimeout(timer);
reject(reason);
});
});
} }
export async function queryToSlashKeys<T>( export async function queryToSlashKeys<T>(url: string): Promise<T> {
url: string, const endpoint = new URL("keys", url);
): Promise<T> {
const endpoint = new URL("keys", url)
endpoint.searchParams.set("cacheBreaker", new Date().getTime() + ""); endpoint.searchParams.set("cacheBreaker", new Date().getTime() + "");
const query = fetch(endpoint.href) const query = fetch(endpoint.href)
@ -73,22 +76,30 @@ export async function queryToSlashKeys<T>(
}) })
.then(getJsonIfOk); .then(getJsonIfOk);
return timeout(3000, query) return timeout(3000, query);
} }
export function buildTermsOfServiceState(tos: GetExchangeTosResult): TermsState { export function buildTermsOfServiceState(
tos: GetExchangeTosResult,
): TermsState {
const content: TermsDocument | undefined = parseTermsOfServiceContent( const content: TermsDocument | undefined = parseTermsOfServiceContent(
tos.contentType, tos.contentType,
tos.content, tos.content,
); );
const status: TermsStatus = buildTermsOfServiceStatus(tos.content, tos.acceptedEtag, tos.currentEtag); const status: TermsStatus = buildTermsOfServiceStatus(
tos.content,
return { content, status, version: tos.currentEtag } tos.acceptedEtag,
tos.currentEtag,
);
return { content, status, version: tos.currentEtag };
} }
export function buildTermsOfServiceStatus(content: string | undefined, acceptedVersion: string | undefined, currentVersion: string | undefined): TermsStatus { export function buildTermsOfServiceStatus(
content: string | undefined,
acceptedVersion: string | undefined,
currentVersion: string | undefined,
): TermsStatus {
return !content return !content
? "notfound" ? "notfound"
: !acceptedVersion : !acceptedVersion

View File

@ -25,15 +25,13 @@ import { SelectFieldHandler, TextFieldHandler } from "../mui/handlers.js";
import { mountHook } from "../test-utils.js"; import { mountHook } from "../test-utils.js";
import { useComponentState } from "./CreateManualWithdraw.js"; import { useComponentState } from "./CreateManualWithdraw.js";
const exchangeListWithARSandUSD = { const exchangeListWithARSandUSD = {
"url1": "USD", url1: "USD",
"url2": "ARS", url2: "ARS",
"url3": "ARS", url3: "ARS",
}; };
const exchangeListEmpty = { const exchangeListEmpty = {};
};
describe("CreateManualWithdraw states", () => { describe("CreateManualWithdraw states", () => {
it("should set noExchangeFound when exchange list is empty", () => { it("should set noExchangeFound when exchange list is empty", () => {
@ -41,9 +39,9 @@ describe("CreateManualWithdraw states", () => {
useComponentState(exchangeListEmpty, undefined, undefined), useComponentState(exchangeListEmpty, undefined, undefined),
); );
const { noExchangeFound } = getLastResultOrThrow() const { noExchangeFound } = getLastResultOrThrow();
expect(noExchangeFound).equal(true) expect(noExchangeFound).equal(true);
}); });
it("should set noExchangeFound when exchange list doesn't include selected currency", () => { it("should set noExchangeFound when exchange list doesn't include selected currency", () => {
@ -51,20 +49,19 @@ describe("CreateManualWithdraw states", () => {
useComponentState(exchangeListWithARSandUSD, undefined, "COL"), useComponentState(exchangeListWithARSandUSD, undefined, "COL"),
); );
const { noExchangeFound } = getLastResultOrThrow() const { noExchangeFound } = getLastResultOrThrow();
expect(noExchangeFound).equal(true) expect(noExchangeFound).equal(true);
}); });
it("should select the first exchange from the list", () => { it("should select the first exchange from the list", () => {
const { getLastResultOrThrow } = mountHook(() => const { getLastResultOrThrow } = mountHook(() =>
useComponentState(exchangeListWithARSandUSD, undefined, undefined), useComponentState(exchangeListWithARSandUSD, undefined, undefined),
); );
const { exchange } = getLastResultOrThrow() const { exchange } = getLastResultOrThrow();
expect(exchange.value).equal("url1") expect(exchange.value).equal("url1");
}); });
it("should select the first exchange with the selected currency", () => { it("should select the first exchange with the selected currency", () => {
@ -72,9 +69,9 @@ describe("CreateManualWithdraw states", () => {
useComponentState(exchangeListWithARSandUSD, undefined, "ARS"), useComponentState(exchangeListWithARSandUSD, undefined, "ARS"),
); );
const { exchange } = getLastResultOrThrow() const { exchange } = getLastResultOrThrow();
expect(exchange.value).equal("url2") expect(exchange.value).equal("url2");
}); });
it("should change the exchange when currency change", async () => { it("should change the exchange when currency change", async () => {
@ -82,22 +79,20 @@ describe("CreateManualWithdraw states", () => {
useComponentState(exchangeListWithARSandUSD, undefined, "ARS"), useComponentState(exchangeListWithARSandUSD, undefined, "ARS"),
); );
{ {
const { exchange, currency } = getLastResultOrThrow() const { exchange, currency } = getLastResultOrThrow();
expect(exchange.value).equal("url2") expect(exchange.value).equal("url2");
if (currency.onChange === undefined) expect.fail(); if (currency.onChange === undefined) expect.fail();
currency.onChange("USD") currency.onChange("USD");
} }
await waitNextUpdate() await waitNextUpdate();
{ {
const { exchange } = getLastResultOrThrow() const { exchange } = getLastResultOrThrow();
expect(exchange.value).equal("url1") expect(exchange.value).equal("url1");
} }
}); });
it("should change the currency when exchange change", async () => { it("should change the currency when exchange change", async () => {
@ -106,22 +101,22 @@ describe("CreateManualWithdraw states", () => {
); );
{ {
const { exchange, currency } = getLastResultOrThrow() const { exchange, currency } = getLastResultOrThrow();
expect(exchange.value).equal("url2") expect(exchange.value).equal("url2");
expect(currency.value).equal("ARS") expect(currency.value).equal("ARS");
if (exchange.onChange === undefined) expect.fail(); if (exchange.onChange === undefined) expect.fail();
exchange.onChange("url1") exchange.onChange("url1");
} }
await waitNextUpdate() await waitNextUpdate();
{ {
const { exchange, currency } = getLastResultOrThrow() const { exchange, currency } = getLastResultOrThrow();
expect(exchange.value).equal("url1") expect(exchange.value).equal("url1");
expect(currency.value).equal("USD") expect(currency.value).equal("USD");
} }
}); });
@ -131,21 +126,23 @@ describe("CreateManualWithdraw states", () => {
); );
{ {
const { amount, parsedAmount } = getLastResultOrThrow() const { amount, parsedAmount } = getLastResultOrThrow();
expect(parsedAmount).equal(undefined) expect(parsedAmount).equal(undefined);
amount.onInput("12") amount.onInput("12");
} }
await waitNextUpdate() await waitNextUpdate();
{ {
const { parsedAmount } = getLastResultOrThrow() const { parsedAmount } = getLastResultOrThrow();
expect(parsedAmount).deep.equals({ expect(parsedAmount).deep.equals({
value: 12, fraction: 0, currency: "ARS" value: 12,
}) fraction: 0,
currency: "ARS",
});
} }
}); });
@ -154,67 +151,79 @@ describe("CreateManualWithdraw states", () => {
useComponentState(exchangeListWithARSandUSD, undefined, "ARS"), useComponentState(exchangeListWithARSandUSD, undefined, "ARS"),
); );
await defaultTestForInputText(waitNextUpdate, () => getLastResultOrThrow().amount) await defaultTestForInputText(
}) waitNextUpdate,
() => getLastResultOrThrow().amount,
);
});
it("should have an exchange selector ", async () => { it("should have an exchange selector ", async () => {
const { getLastResultOrThrow, waitNextUpdate } = mountHook(() => const { getLastResultOrThrow, waitNextUpdate } = mountHook(() =>
useComponentState(exchangeListWithARSandUSD, undefined, "ARS"), useComponentState(exchangeListWithARSandUSD, undefined, "ARS"),
); );
await defaultTestForInputSelect(waitNextUpdate, () => getLastResultOrThrow().exchange) await defaultTestForInputSelect(
}) waitNextUpdate,
() => getLastResultOrThrow().exchange,
);
});
it("should have a currency selector ", async () => { it("should have a currency selector ", async () => {
const { getLastResultOrThrow, waitNextUpdate } = mountHook(() => const { getLastResultOrThrow, waitNextUpdate } = mountHook(() =>
useComponentState(exchangeListWithARSandUSD, undefined, "ARS"), useComponentState(exchangeListWithARSandUSD, undefined, "ARS"),
); );
await defaultTestForInputSelect(waitNextUpdate, () => getLastResultOrThrow().currency) await defaultTestForInputSelect(
}) waitNextUpdate,
() => getLastResultOrThrow().currency,
);
});
}); });
async function defaultTestForInputText(
async function defaultTestForInputText(awaiter: () => Promise<void>, getField: () => TextFieldHandler): Promise<void> { awaiter: () => Promise<void>,
let nextValue = '' getField: () => TextFieldHandler,
): Promise<void> {
let nextValue = "";
{ {
const field = getField() const field = getField();
const initialValue = field.value; const initialValue = field.value;
nextValue = `${initialValue} something else` nextValue = `${initialValue} something else`;
field.onInput(nextValue) field.onInput(nextValue);
} }
await awaiter() await awaiter();
{ {
const field = getField() const field = getField();
expect(field.value).equal(nextValue) expect(field.value).equal(nextValue);
} }
} }
async function defaultTestForInputSelect(
async function defaultTestForInputSelect(awaiter: () => Promise<void>, getField: () => SelectFieldHandler): Promise<void> { awaiter: () => Promise<void>,
let nextValue = '' getField: () => SelectFieldHandler,
): Promise<void> {
let nextValue = "";
{ {
const field = getField(); const field = getField();
const initialValue = field.value; const initialValue = field.value;
const keys = Object.keys(field.list) const keys = Object.keys(field.list);
const nextIdx = keys.indexOf(initialValue) + 1 const nextIdx = keys.indexOf(initialValue) + 1;
if (keys.length < nextIdx) { if (keys.length < nextIdx) {
throw new Error('no enough values') throw new Error("no enough values");
} }
nextValue = keys[nextIdx] nextValue = keys[nextIdx];
if (field.onChange === undefined) expect.fail(); if (field.onChange === undefined) expect.fail();
field.onChange(nextValue) field.onChange(nextValue);
} }
await awaiter() await awaiter();
{ {
const field = getField(); const field = getField();
expect(field.value).equal(nextValue) expect(field.value).equal(nextValue);
} }
} }

View File

@ -19,392 +19,412 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { Amounts, Balance, BalancesResponse, parsePaytoUri } from "@gnu-taler/taler-util"; import {
Amounts,
Balance,
BalancesResponse,
parsePaytoUri,
} from "@gnu-taler/taler-util";
import { DepositGroupFees } from "@gnu-taler/taler-wallet-core/src/operations/deposits"; import { DepositGroupFees } from "@gnu-taler/taler-wallet-core/src/operations/deposits";
import { expect } from "chai"; import { expect } from "chai";
import { mountHook } from "../test-utils.js"; import { mountHook } from "../test-utils.js";
import { useComponentState } from "./DepositPage.js"; import { useComponentState } from "./DepositPage.js";
import * as wxApi from "../wxApi.js"; import * as wxApi from "../wxApi.js";
const currency = "EUR";
const currency = "EUR"
const withoutFee = async (): Promise<DepositGroupFees> => ({ const withoutFee = async (): Promise<DepositGroupFees> => ({
coin: Amounts.parseOrThrow(`${currency}:0`), coin: Amounts.parseOrThrow(`${currency}:0`),
wire: Amounts.parseOrThrow(`${currency}:0`), wire: Amounts.parseOrThrow(`${currency}:0`),
refresh: Amounts.parseOrThrow(`${currency}:0`) refresh: Amounts.parseOrThrow(`${currency}:0`),
}) });
const withSomeFee = async (): Promise<DepositGroupFees> => ({ const withSomeFee = async (): Promise<DepositGroupFees> => ({
coin: Amounts.parseOrThrow(`${currency}:1`), coin: Amounts.parseOrThrow(`${currency}:1`),
wire: Amounts.parseOrThrow(`${currency}:1`), wire: Amounts.parseOrThrow(`${currency}:1`),
refresh: Amounts.parseOrThrow(`${currency}:1`) refresh: Amounts.parseOrThrow(`${currency}:1`),
}) });
const freeJustForIBAN = async (account: string): Promise<DepositGroupFees> => /IBAN/i.test(account) ? withoutFee() : withSomeFee() const freeJustForIBAN = async (account: string): Promise<DepositGroupFees> =>
/IBAN/i.test(account) ? withoutFee() : withSomeFee();
const someBalance = [{ const someBalance = [
available: 'EUR:10' {
} as Balance] available: "EUR:10",
} as Balance,
];
const nullFunction: any = () => null; const nullFunction: any = () => null;
type VoidFunction = () => void; type VoidFunction = () => void;
describe("DepositPage states", () => { describe("DepositPage states", () => {
it("should have status 'no-balance' when balance is empty", async () => { it("should have status 'no-balance' when balance is empty", async () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() =>
useComponentState(currency, nullFunction, nullFunction, { useComponentState(currency, nullFunction, nullFunction, {
getBalance: async () => ({ getBalance: async () =>
balances: [{ available: `${currency}:0`, }] ({
balances: [{ available: `${currency}:0` }],
} as Partial<BalancesResponse>), } as Partial<BalancesResponse>),
listKnownBankAccounts: async () => ({ accounts: [] }) listKnownBankAccounts: async () => ({ accounts: [] }),
} as Partial<typeof wxApi> as any) } as Partial<typeof wxApi> as any),
); );
{ {
const { status } = getLastResultOrThrow() const { status } = getLastResultOrThrow();
expect(status).equal("loading") expect(status).equal("loading");
} }
await waitNextUpdate() await waitNextUpdate();
{ {
const { status } = getLastResultOrThrow() const { status } = getLastResultOrThrow();
expect(status).equal("no-balance") expect(status).equal("no-balance");
} }
await assertNoPendingUpdate() await assertNoPendingUpdate();
}); });
it("should have status 'no-accounts' when balance is not empty and accounts is empty", async () => { it("should have status 'no-accounts' when balance is not empty and accounts is empty", async () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() =>
useComponentState(currency, nullFunction, nullFunction, { useComponentState(currency, nullFunction, nullFunction, {
getBalance: async () => ({ getBalance: async () =>
balances: [{ available: `${currency}:1`, }] ({
balances: [{ available: `${currency}:1` }],
} as Partial<BalancesResponse>), } as Partial<BalancesResponse>),
listKnownBankAccounts: async () => ({ accounts: [] }) listKnownBankAccounts: async () => ({ accounts: [] }),
} as Partial<typeof wxApi> as any) } as Partial<typeof wxApi> as any),
); );
{ {
const { status } = getLastResultOrThrow() const { status } = getLastResultOrThrow();
expect(status).equal("loading") expect(status).equal("loading");
} }
await waitNextUpdate() await waitNextUpdate();
{ {
const r = getLastResultOrThrow() const r = getLastResultOrThrow();
if (r.status !== "no-accounts") expect.fail(); if (r.status !== "no-accounts") expect.fail();
expect(r.cancelHandler.onClick).not.undefined; expect(r.cancelHandler.onClick).not.undefined;
} }
await assertNoPendingUpdate() await assertNoPendingUpdate();
}); });
const ibanPayto = parsePaytoUri("payto://iban/ES8877998399652238")!; const ibanPayto = parsePaytoUri("payto://iban/ES8877998399652238")!;
const talerBankPayto = parsePaytoUri("payto://x-taler-bank/ES8877998399652238")!; const talerBankPayto = parsePaytoUri(
"payto://x-taler-bank/ES8877998399652238",
)!;
it("should have status 'ready' but unable to deposit ", async () => { it("should have status 'ready' but unable to deposit ", async () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() =>
useComponentState(currency, nullFunction, nullFunction, { useComponentState(currency, nullFunction, nullFunction, {
getBalance: async () => ({ getBalance: async () =>
balances: [{ available: `${currency}:1`, }] ({
balances: [{ available: `${currency}:1` }],
} as Partial<BalancesResponse>), } as Partial<BalancesResponse>),
listKnownBankAccounts: async () => ({ accounts: [ibanPayto] }) listKnownBankAccounts: async () => ({ accounts: [ibanPayto] }),
} as Partial<typeof wxApi> as any) } as Partial<typeof wxApi> as any),
); );
{ {
const { status } = getLastResultOrThrow() const { status } = getLastResultOrThrow();
expect(status).equal("loading") expect(status).equal("loading");
} }
await waitNextUpdate() await waitNextUpdate();
{ {
const r = getLastResultOrThrow() const r = getLastResultOrThrow();
if (r.status !== "ready") expect.fail(); if (r.status !== "ready") expect.fail();
expect(r.cancelHandler.onClick).not.undefined; expect(r.cancelHandler.onClick).not.undefined;
expect(r.currency).eq(currency); expect(r.currency).eq(currency);
expect(r.account.value).eq("0") expect(r.account.value).eq("0");
expect(r.amount.value).eq("0") expect(r.amount.value).eq("0");
expect(r.depositHandler.onClick).undefined; expect(r.depositHandler.onClick).undefined;
} }
await assertNoPendingUpdate() await assertNoPendingUpdate();
}); });
it("should not be able to deposit more than the balance ", async () => { it("should not be able to deposit more than the balance ", async () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() =>
useComponentState(currency, nullFunction, nullFunction, { useComponentState(currency, nullFunction, nullFunction, {
getBalance: async () => ({ getBalance: async () =>
balances: [{ available: `${currency}:1`, }] ({
balances: [{ available: `${currency}:1` }],
} as Partial<BalancesResponse>), } as Partial<BalancesResponse>),
listKnownBankAccounts: async () => ({ accounts: [ibanPayto] }), listKnownBankAccounts: async () => ({ accounts: [ibanPayto] }),
getFeeForDeposit: withoutFee getFeeForDeposit: withoutFee,
} as Partial<typeof wxApi> as any) } as Partial<typeof wxApi> as any),
); );
{ {
const { status } = getLastResultOrThrow() const { status } = getLastResultOrThrow();
expect(status).equal("loading") expect(status).equal("loading");
} }
await waitNextUpdate() await waitNextUpdate();
{ {
const r = getLastResultOrThrow() const r = getLastResultOrThrow();
if (r.status !== "ready") expect.fail(); if (r.status !== "ready") expect.fail();
expect(r.cancelHandler.onClick).not.undefined; expect(r.cancelHandler.onClick).not.undefined;
expect(r.currency).eq(currency); expect(r.currency).eq(currency);
expect(r.account.value).eq("0") expect(r.account.value).eq("0");
expect(r.amount.value).eq("0") expect(r.amount.value).eq("0");
expect(r.depositHandler.onClick).undefined; expect(r.depositHandler.onClick).undefined;
expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`)) expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`));
r.amount.onInput("10") r.amount.onInput("10");
} }
await waitNextUpdate() await waitNextUpdate();
{ {
const r = getLastResultOrThrow() const r = getLastResultOrThrow();
if (r.status !== "ready") expect.fail(); if (r.status !== "ready") expect.fail();
expect(r.cancelHandler.onClick).not.undefined; expect(r.cancelHandler.onClick).not.undefined;
expect(r.currency).eq(currency); expect(r.currency).eq(currency);
expect(r.account.value).eq("0") expect(r.account.value).eq("0");
expect(r.amount.value).eq("10") expect(r.amount.value).eq("10");
expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`)) expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`));
expect(r.depositHandler.onClick).undefined; expect(r.depositHandler.onClick).undefined;
} }
await assertNoPendingUpdate() await assertNoPendingUpdate();
}); });
it("should calculate the fee upon entering amount ", async () => { it("should calculate the fee upon entering amount ", async () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() =>
useComponentState(currency, nullFunction, nullFunction, { useComponentState(currency, nullFunction, nullFunction, {
getBalance: async () => ({ getBalance: async () =>
balances: [{ available: `${currency}:1`, }] ({
balances: [{ available: `${currency}:1` }],
} as Partial<BalancesResponse>), } as Partial<BalancesResponse>),
listKnownBankAccounts: async () => ({ accounts: [ibanPayto] }), listKnownBankAccounts: async () => ({ accounts: [ibanPayto] }),
getFeeForDeposit: withSomeFee getFeeForDeposit: withSomeFee,
} as Partial<typeof wxApi> as any) } as Partial<typeof wxApi> as any),
); );
{ {
const { status } = getLastResultOrThrow() const { status } = getLastResultOrThrow();
expect(status).equal("loading") expect(status).equal("loading");
} }
await waitNextUpdate() await waitNextUpdate();
{ {
const r = getLastResultOrThrow() const r = getLastResultOrThrow();
if (r.status !== "ready") expect.fail(); if (r.status !== "ready") expect.fail();
expect(r.cancelHandler.onClick).not.undefined; expect(r.cancelHandler.onClick).not.undefined;
expect(r.currency).eq(currency); expect(r.currency).eq(currency);
expect(r.account.value).eq("0") expect(r.account.value).eq("0");
expect(r.amount.value).eq("0") expect(r.amount.value).eq("0");
expect(r.depositHandler.onClick).undefined; expect(r.depositHandler.onClick).undefined;
expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`)) expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`));
r.amount.onInput("10") r.amount.onInput("10");
} }
await waitNextUpdate() await waitNextUpdate();
{ {
const r = getLastResultOrThrow() const r = getLastResultOrThrow();
if (r.status !== "ready") expect.fail(); if (r.status !== "ready") expect.fail();
expect(r.cancelHandler.onClick).not.undefined; expect(r.cancelHandler.onClick).not.undefined;
expect(r.currency).eq(currency); expect(r.currency).eq(currency);
expect(r.account.value).eq("0") expect(r.account.value).eq("0");
expect(r.amount.value).eq("10") expect(r.amount.value).eq("10");
expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`)) expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`));
expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:7`)) expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:7`));
expect(r.depositHandler.onClick).undefined; expect(r.depositHandler.onClick).undefined;
} }
await assertNoPendingUpdate() await assertNoPendingUpdate();
}); });
it("should calculate the fee upon selecting account ", async () => { it("should calculate the fee upon selecting account ", async () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() =>
useComponentState(currency, nullFunction, nullFunction, { useComponentState(currency, nullFunction, nullFunction, {
getBalance: async () => ({ getBalance: async () =>
balances: [{ available: `${currency}:1`, }] ({
balances: [{ available: `${currency}:1` }],
} as Partial<BalancesResponse>), } as Partial<BalancesResponse>),
listKnownBankAccounts: async () => ({ accounts: [ibanPayto, talerBankPayto] }), listKnownBankAccounts: async () => ({
getFeeForDeposit: freeJustForIBAN accounts: [ibanPayto, talerBankPayto],
} as Partial<typeof wxApi> as any) }),
getFeeForDeposit: freeJustForIBAN,
} as Partial<typeof wxApi> as any),
); );
{ {
const { status } = getLastResultOrThrow() const { status } = getLastResultOrThrow();
expect(status).equal("loading") expect(status).equal("loading");
} }
await waitNextUpdate() await waitNextUpdate();
{ {
const r = getLastResultOrThrow() const r = getLastResultOrThrow();
if (r.status !== "ready") expect.fail(); if (r.status !== "ready") expect.fail();
expect(r.cancelHandler.onClick).not.undefined; expect(r.cancelHandler.onClick).not.undefined;
expect(r.currency).eq(currency); expect(r.currency).eq(currency);
expect(r.account.value).eq("0") expect(r.account.value).eq("0");
expect(r.amount.value).eq("0") expect(r.amount.value).eq("0");
expect(r.depositHandler.onClick).undefined; expect(r.depositHandler.onClick).undefined;
expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`)) expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`));
if (r.account.onChange === undefined) expect.fail(); if (r.account.onChange === undefined) expect.fail();
r.account.onChange("1") r.account.onChange("1");
} }
await waitNextUpdate() await waitNextUpdate();
{ {
const r = getLastResultOrThrow() const r = getLastResultOrThrow();
if (r.status !== "ready") expect.fail(); if (r.status !== "ready") expect.fail();
expect(r.cancelHandler.onClick).not.undefined; expect(r.cancelHandler.onClick).not.undefined;
expect(r.currency).eq(currency); expect(r.currency).eq(currency);
expect(r.account.value).eq("1") expect(r.account.value).eq("1");
expect(r.amount.value).eq("0") expect(r.amount.value).eq("0");
expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`)) expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`));
expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:0`)) expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:0`));
expect(r.depositHandler.onClick).undefined; expect(r.depositHandler.onClick).undefined;
r.amount.onInput("10") r.amount.onInput("10");
} }
await waitNextUpdate() await waitNextUpdate();
{ {
const r = getLastResultOrThrow() const r = getLastResultOrThrow();
if (r.status !== "ready") expect.fail(); if (r.status !== "ready") expect.fail();
expect(r.cancelHandler.onClick).not.undefined; expect(r.cancelHandler.onClick).not.undefined;
expect(r.currency).eq(currency); expect(r.currency).eq(currency);
expect(r.account.value).eq("1") expect(r.account.value).eq("1");
expect(r.amount.value).eq("10") expect(r.amount.value).eq("10");
expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`)) expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`));
expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:7`)) expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:7`));
expect(r.depositHandler.onClick).undefined; expect(r.depositHandler.onClick).undefined;
if (r.account.onChange === undefined) expect.fail(); if (r.account.onChange === undefined) expect.fail();
r.account.onChange("0") r.account.onChange("0");
} }
await waitNextUpdate() await waitNextUpdate();
{ {
const r = getLastResultOrThrow() const r = getLastResultOrThrow();
if (r.status !== "ready") expect.fail(); if (r.status !== "ready") expect.fail();
expect(r.cancelHandler.onClick).not.undefined; expect(r.cancelHandler.onClick).not.undefined;
expect(r.currency).eq(currency); expect(r.currency).eq(currency);
expect(r.account.value).eq("0") expect(r.account.value).eq("0");
expect(r.amount.value).eq("10") expect(r.amount.value).eq("10");
expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`)) expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`));
expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:10`)) expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:10`));
expect(r.depositHandler.onClick).undefined; expect(r.depositHandler.onClick).undefined;
} }
await assertNoPendingUpdate() await assertNoPendingUpdate();
}); });
it("should be able to deposit if has the enough balance ", async () => { it("should be able to deposit if has the enough balance ", async () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() =>
useComponentState(currency, nullFunction, nullFunction, { useComponentState(currency, nullFunction, nullFunction, {
getBalance: async () => ({ getBalance: async () =>
balances: [{ available: `${currency}:15`, }] ({
balances: [{ available: `${currency}:15` }],
} as Partial<BalancesResponse>), } as Partial<BalancesResponse>),
listKnownBankAccounts: async () => ({ accounts: [ibanPayto] }), listKnownBankAccounts: async () => ({ accounts: [ibanPayto] }),
getFeeForDeposit: withSomeFee getFeeForDeposit: withSomeFee,
} as Partial<typeof wxApi> as any) } as Partial<typeof wxApi> as any),
); );
{ {
const { status } = getLastResultOrThrow() const { status } = getLastResultOrThrow();
expect(status).equal("loading") expect(status).equal("loading");
} }
await waitNextUpdate() await waitNextUpdate();
{ {
const r = getLastResultOrThrow() const r = getLastResultOrThrow();
if (r.status !== "ready") expect.fail(); if (r.status !== "ready") expect.fail();
expect(r.cancelHandler.onClick).not.undefined; expect(r.cancelHandler.onClick).not.undefined;
expect(r.currency).eq(currency); expect(r.currency).eq(currency);
expect(r.account.value).eq("0") expect(r.account.value).eq("0");
expect(r.amount.value).eq("0") expect(r.amount.value).eq("0");
expect(r.depositHandler.onClick).undefined; expect(r.depositHandler.onClick).undefined;
expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`)) expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`));
r.amount.onInput("10") r.amount.onInput("10");
} }
await waitNextUpdate() await waitNextUpdate();
{ {
const r = getLastResultOrThrow() const r = getLastResultOrThrow();
if (r.status !== "ready") expect.fail(); if (r.status !== "ready") expect.fail();
expect(r.cancelHandler.onClick).not.undefined; expect(r.cancelHandler.onClick).not.undefined;
expect(r.currency).eq(currency); expect(r.currency).eq(currency);
expect(r.account.value).eq("0") expect(r.account.value).eq("0");
expect(r.amount.value).eq("10") expect(r.amount.value).eq("10");
expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`)) expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`));
expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:7`)) expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:7`));
expect(r.depositHandler.onClick).not.undefined; expect(r.depositHandler.onClick).not.undefined;
r.amount.onInput("13") r.amount.onInput("13");
} }
await waitNextUpdate() await waitNextUpdate();
{ {
const r = getLastResultOrThrow() const r = getLastResultOrThrow();
if (r.status !== "ready") expect.fail(); if (r.status !== "ready") expect.fail();
expect(r.cancelHandler.onClick).not.undefined; expect(r.cancelHandler.onClick).not.undefined;
expect(r.currency).eq(currency); expect(r.currency).eq(currency);
expect(r.account.value).eq("0") expect(r.account.value).eq("0");
expect(r.amount.value).eq("13") expect(r.amount.value).eq("13");
expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`)) expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`));
expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:10`)) expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:10`));
expect(r.depositHandler.onClick).not.undefined; expect(r.depositHandler.onClick).not.undefined;
r.amount.onInput("15") r.amount.onInput("15");
} }
await waitNextUpdate() await waitNextUpdate();
{ {
const r = getLastResultOrThrow() const r = getLastResultOrThrow();
if (r.status !== "ready") expect.fail(); if (r.status !== "ready") expect.fail();
expect(r.cancelHandler.onClick).not.undefined; expect(r.cancelHandler.onClick).not.undefined;
expect(r.currency).eq(currency); expect(r.currency).eq(currency);
expect(r.account.value).eq("0") expect(r.account.value).eq("0");
expect(r.amount.value).eq("15") expect(r.amount.value).eq("15");
expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`)) expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`));
expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:12`)) expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:12`));
expect(r.depositHandler.onClick).not.undefined; expect(r.depositHandler.onClick).not.undefined;
r.amount.onInput("17") r.amount.onInput("17");
} }
await waitNextUpdate() await waitNextUpdate();
{ {
const r = getLastResultOrThrow() const r = getLastResultOrThrow();
if (r.status !== "ready") expect.fail(); if (r.status !== "ready") expect.fail();
expect(r.cancelHandler.onClick).not.undefined; expect(r.cancelHandler.onClick).not.undefined;
expect(r.currency).eq(currency); expect(r.currency).eq(currency);
expect(r.account.value).eq("0") expect(r.account.value).eq("0");
expect(r.amount.value).eq("17") expect(r.amount.value).eq("17");
expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`)) expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`));
expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:14`)) expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:14`));
expect(r.depositHandler.onClick).undefined; expect(r.depositHandler.onClick).undefined;
} }
await assertNoPendingUpdate() await assertNoPendingUpdate();
}); });
}); });

View File

@ -151,7 +151,6 @@ export function runGarbageCollector(): Promise<void> {
return callBackend("run-gc", {}); return callBackend("run-gc", {});
} }
export function getFeeForDeposit( export function getFeeForDeposit(
depositPaytoUri: string, depositPaytoUri: string,
amount: AmountString, amount: AmountString,
@ -338,7 +337,7 @@ export function acceptWithdrawal(
return callBackend("acceptBankIntegratedWithdrawal", { return callBackend("acceptBankIntegratedWithdrawal", {
talerWithdrawUri, talerWithdrawUri,
exchangeBaseUrl: selectedExchange, exchangeBaseUrl: selectedExchange,
restrictAge restrictAge,
}); });
} }
@ -356,7 +355,7 @@ export function acceptManualWithdrawal(
return callBackend("acceptManualWithdrawal", { return callBackend("acceptManualWithdrawal", {
amount, amount,
exchangeBaseUrl, exchangeBaseUrl,
restrictAge restrictAge,
}); });
} }
@ -432,11 +431,12 @@ export function addExchange(req: AddExchangeRequest): Promise<void> {
return callBackend("addExchange", req); return callBackend("addExchange", req);
} }
export function prepareRefund(req: PrepareRefundRequest): Promise<PrepareRefundResult> { export function prepareRefund(
req: PrepareRefundRequest,
): Promise<PrepareRefundResult> {
return callBackend("prepareRefund", req); return callBackend("prepareRefund", req);
} }
export function prepareTip(req: PrepareTipRequest): Promise<PrepareTipResult> { export function prepareTip(req: PrepareTipRequest): Promise<PrepareTipResult> {
return callBackend("prepareTip", req); return callBackend("prepareTip", req);
} }

View File

@ -26,9 +26,11 @@
import { import {
classifyTalerUri, classifyTalerUri,
CoreApiResponse, CoreApiResponse,
CoreApiResponseSuccess, Logger, TalerErrorCode, CoreApiResponseSuccess,
Logger,
TalerErrorCode,
TalerUriType, TalerUriType,
WalletDiagnostics WalletDiagnostics,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { import {
DbAccess, DbAccess,
@ -40,7 +42,7 @@ import {
openPromise, openPromise,
openTalerDatabase, openTalerDatabase,
Wallet, Wallet,
WalletStoresV1 WalletStoresV1,
} from "@gnu-taler/taler-wallet-core"; } from "@gnu-taler/taler-wallet-core";
import { SetTimeoutTimerAPI } from "@gnu-taler/taler-wallet-core"; import { SetTimeoutTimerAPI } from "@gnu-taler/taler-wallet-core";
import { BrowserCryptoWorkerFactory } from "./browserCryptoWorkerFactory.js"; import { BrowserCryptoWorkerFactory } from "./browserCryptoWorkerFactory.js";
@ -133,14 +135,14 @@ async function dispatch(
break; break;
} }
case "run-gc": { case "run-gc": {
logger.info("gc") logger.info("gc");
const dump = await exportDb(currentDatabase!.idbHandle()); const dump = await exportDb(currentDatabase!.idbHandle());
await deleteTalerDatabase(indexedDB as any); await deleteTalerDatabase(indexedDB as any);
logger.info("cleaned") logger.info("cleaned");
await reinitWallet(); await reinitWallet();
logger.info("init") logger.info("init");
await importDb(currentDatabase!.idbHandle(), dump) await importDb(currentDatabase!.idbHandle(), dump);
logger.info("imported") logger.info("imported");
r = wrapResponse({ result: true }); r = wrapResponse({ result: true });
break; break;
} }
@ -156,7 +158,9 @@ async function dispatch(
platform.registerTalerHeaderListener(parseTalerUriAndRedirect); platform.registerTalerHeaderListener(parseTalerUriAndRedirect);
r = wrapResponse({ newValue: true }); r = wrapResponse({ newValue: true });
} else { } else {
const rem = await platform.getPermissionsApi().removeHostPermissions(); const rem = await platform
.getPermissionsApi()
.removeHostPermissions();
logger.trace("permissions removed:", rem); logger.trace("permissions removed:", rem);
r = wrapResponse({ newVal: false }); r = wrapResponse({ newVal: false });
} }
@ -184,7 +188,7 @@ async function dispatch(
sendResponse(r); sendResponse(r);
} catch (e) { } catch (e) {
logger.error(`Error sending operation: ${req.operation}`, e) logger.error(`Error sending operation: ${req.operation}`, e);
// might fail if tab disconnected // might fail if tab disconnected
} }
} }
@ -218,7 +222,12 @@ async function reinitWallet(): Promise<void> {
} }
logger.info("Setting up wallet"); logger.info("Setting up wallet");
const wallet = await Wallet.create(currentDatabase, httpLib, timer, cryptoWorker); const wallet = await Wallet.create(
currentDatabase,
httpLib,
timer,
cryptoWorker,
);
try { try {
await wallet.handleCoreApiRequest("initWallet", "native-init", {}); await wallet.handleCoreApiRequest("initWallet", "native-init", {});
} catch (e) { } catch (e) {
@ -228,14 +237,14 @@ async function reinitWallet(): Promise<void> {
} }
wallet.addNotificationListener((x) => { wallet.addNotificationListener((x) => {
const message: MessageFromBackend = { type: x.type }; const message: MessageFromBackend = { type: x.type };
platform.sendMessageToAllChannels(message) platform.sendMessageToAllChannels(message);
}); });
platform.keepAlive(() => { platform.keepAlive(() => {
return wallet.runTaskLoop().catch((e) => { return wallet.runTaskLoop().catch((e) => {
logger.error("error during wallet task loop", e); logger.error("error during wallet task loop", e);
}); });
}) });
// Useful for debugging in the background page. // Useful for debugging in the background page.
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
(window as any).talerWallet = wallet; (window as any).talerWallet = wallet;
@ -279,14 +288,13 @@ function parseTalerUriAndRedirect(tabId: number, talerUri: string): void {
} }
} }
/** /**
* Main function to run for the WebExtension backend. * Main function to run for the WebExtension backend.
* *
* Sets up all event handlers and other machinery. * Sets up all event handlers and other machinery.
*/ */
export async function wxMain(): Promise<void> { export async function wxMain(): Promise<void> {
logger.trace("starting") logger.trace("starting");
const afterWalletIsInitialized = reinitWallet(); const afterWalletIsInitialized = reinitWallet();
platform.registerReloadOnNewVersion(); platform.registerReloadOnNewVersion();
@ -297,9 +305,9 @@ export async function wxMain(): Promise<void> {
afterWalletIsInitialized.then(() => { afterWalletIsInitialized.then(() => {
dispatch(message, sender, callback); dispatch(message, sender, callback);
}); });
}) });
platform.registerAllIncomingConnections() platform.registerAllIncomingConnections();
try { try {
platform.registerTalerHeaderListener(parseTalerUriAndRedirect); platform.registerTalerHeaderListener(parseTalerUriAndRedirect);
@ -311,7 +319,10 @@ export async function wxMain(): Promise<void> {
// modification of permissions. // modification of permissions.
platform.getPermissionsApi().addPermissionsListener((perm, lastError) => { platform.getPermissionsApi().addPermissionsListener((perm, lastError) => {
if (lastError) { if (lastError) {
logger.error(`there was a problem trying to get permission ${perm}`, lastError); logger.error(
`there was a problem trying to get permission ${perm}`,
lastError,
);
return; return;
} }
platform.registerTalerHeaderListener(parseTalerUriAndRedirect); platform.registerTalerHeaderListener(parseTalerUriAndRedirect);

View File

@ -2,7 +2,7 @@
"compilerOptions": { "compilerOptions": {
"composite": true, "composite": true,
"lib": [ "lib": [
"es6", "es2021",
"DOM" "DOM"
], ],
"jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ "jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */