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 { wxMain } from "./wxBackend.js";
console.log("Wallet setup for Dev API")
setupPlatform(devAPI)
console.log("Wallet setup for Dev API");
setupPlatform(devAPI);
try {
platform.registerOnInstalled(() => {
platform.openWalletPage("/welcome")
})
platform.openWalletPage("/welcome");
});
} catch (e) {
console.error(e);
}
platform.notifyWhenAppIsReady(() => {
wxMain();
})
});

View File

@ -28,22 +28,24 @@ import chromeAPI from "./platform/chrome.js";
import firefoxAPI from "./platform/firefox.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
if (isFirefox) {
console.log("Wallet setup for Firefox API")
setupPlatform(firefoxAPI)
console.log("Wallet setup for Firefox API");
setupPlatform(firefoxAPI);
} else {
console.log("Wallet setup for Chrome API")
setupPlatform(chromeAPI)
console.log("Wallet setup for Chrome API");
setupPlatform(chromeAPI);
}
try {
platform.registerOnInstalled(() => {
platform.openWalletPage("/welcome")
})
platform.openWalletPage("/welcome");
});
} catch (e) {
console.error(e);
}
@ -51,4 +53,4 @@ try {
// setGlobalLogLevelFromString("trace")
platform.notifyWhenAppIsReady(() => {
wxMain();
})
});

View File

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

View File

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

View File

@ -32,18 +32,31 @@ const Context = createContext<Type>({
/**
* Inversion of control Context
*
* This context act as a proxy between API that need to be replaced in
*
* This context act as a proxy between API that need to be replaced in
* different environments
*
* @returns
*
* @returns
*/
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 });
};
export const IoCProviderForRuntime = ({ children }: { children: any }): VNode => {
return h(Context.Provider, { value: { findTalerUriInActiveTab: platform.findTalerUriInActiveTab }, children });
export const IoCProviderForRuntime = ({
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 {
lang: string;
supportedLang: { [id in keyof typeof supportedLang]: string }
supportedLang: { [id in keyof typeof supportedLang]: string };
changeLanguage: (l: string) => void;
i18n: typeof i18n;
isSaved: boolean;
@ -47,7 +47,6 @@ const supportedLang = {
navigator: "Defined by navigator",
};
const initial = {
lang: "en",
supportedLang,
@ -84,7 +83,10 @@ export const TranslationProvider = ({
} else {
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);

View File

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

View File

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

View File

@ -19,7 +19,12 @@
* @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 { mountHook } from "../test-utils.js";
import { SubsHandler } from "./Pay.test.js";
@ -29,146 +34,151 @@ import { useComponentState } from "./Refund.jsx";
describe("Refund CTA states", () => {
it("should tell the user that the URI is missing", async () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() =>
useComponentState(undefined, {
prepareRefund: async () => ({}),
applyRefund: async () => ({}),
onUpdateNotification: async () => ({})
} as any),
);
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() =>
useComponentState(undefined, {
prepareRefund: async () => ({}),
applyRefund: async () => ({}),
onUpdateNotification: async () => ({}),
} as any),
);
{
const { status, hook } = getLastResultOrThrow()
expect(status).equals('loading')
const { status, hook } = getLastResultOrThrow();
expect(status).equals("loading");
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.hasError) expect.fail();
if (hook.operational) expect.fail();
expect(hook.message).eq("ERROR_NO-URI-FOR-REFUND");
}
await assertNoPendingUpdate()
await assertNoPendingUpdate();
});
it("should be ready after loading", async () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() =>
useComponentState("taler://refund/asdasdas", {
prepareRefund: async () => ({
effectivePaid: 'EUR:2',
awaiting: 'EUR:2',
gone: 'EUR:0',
granted: 'EUR:0',
pending: false,
proposalId: '1',
info: {
contractTermsHash: '123',
merchant: {
name: 'the merchant name'
},
orderId: 'orderId1',
summary: 'the sumary'
}
} as PrepareRefundResult as any),
applyRefund: async () => ({}),
onUpdateNotification: async () => ({})
} as any),
);
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() =>
useComponentState("taler://refund/asdasdas", {
prepareRefund: async () =>
({
effectivePaid: "EUR:2",
awaiting: "EUR:2",
gone: "EUR:0",
granted: "EUR:0",
pending: false,
proposalId: "1",
info: {
contractTermsHash: "123",
merchant: {
name: "the merchant name",
},
orderId: "orderId1",
summary: "the sumary",
},
} as PrepareRefundResult as any),
applyRefund: async () => ({}),
onUpdateNotification: async () => ({}),
} as any),
);
{
const { status, hook } = getLastResultOrThrow()
expect(status).equals('loading')
const { status, hook } = getLastResultOrThrow();
expect(status).equals("loading");
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();
expect(state.accept.onClick).not.undefined;
expect(state.ignore.onClick).not.undefined;
expect(state.merchantName).eq('the merchant name');
expect(state.orderId).eq('orderId1');
expect(state.merchantName).eq("the merchant name");
expect(state.orderId).eq("orderId1");
expect(state.products).undefined;
}
await assertNoPendingUpdate()
await assertNoPendingUpdate();
});
it("should be ignored after clicking the ignore button", async () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() =>
useComponentState("taler://refund/asdasdas", {
prepareRefund: async () => ({
effectivePaid: 'EUR:2',
awaiting: 'EUR:2',
gone: 'EUR:0',
granted: 'EUR:0',
pending: false,
proposalId: '1',
info: {
contractTermsHash: '123',
merchant: {
name: 'the merchant name'
},
orderId: 'orderId1',
summary: 'the sumary'
}
} as PrepareRefundResult as any),
applyRefund: async () => ({}),
onUpdateNotification: async () => ({})
} as any),
);
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() =>
useComponentState("taler://refund/asdasdas", {
prepareRefund: async () =>
({
effectivePaid: "EUR:2",
awaiting: "EUR:2",
gone: "EUR:0",
granted: "EUR:0",
pending: false,
proposalId: "1",
info: {
contractTermsHash: "123",
merchant: {
name: "the merchant name",
},
orderId: "orderId1",
summary: "the sumary",
},
} as PrepareRefundResult as any),
applyRefund: async () => ({}),
onUpdateNotification: async () => ({}),
} as any),
);
{
const { status, hook } = getLastResultOrThrow()
expect(status).equals('loading')
const { status, hook } = getLastResultOrThrow();
expect(status).equals("loading");
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();
expect(state.accept.onClick).not.undefined;
expect(state.merchantName).eq('the merchant name');
expect(state.orderId).eq('orderId1');
expect(state.merchantName).eq("the merchant name");
expect(state.orderId).eq("orderId1");
expect(state.products).undefined;
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();
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 () => {
let granted = Amounts.getZero('EUR')
const unit: AmountJson = { currency: 'EUR', value: 1, fraction: 0 }
const refunded: AmountJson = { currency: 'EUR', value: 2, fraction: 0 }
let awaiting: AmountJson = refunded
let granted = Amounts.getZero("EUR");
const unit: AmountJson = { currency: "EUR", value: 1, fraction: 0 };
const refunded: AmountJson = { currency: "EUR", value: 2, fraction: 0 };
let awaiting: AmountJson = refunded;
let pending = true;
const subscriptions = new SubsHandler();
@ -177,80 +187,82 @@ describe("Refund CTA states", () => {
granted = Amounts.add(granted, unit).amount;
pending = granted.value < refunded.value;
awaiting = Amounts.sub(refunded, granted).amount;
subscriptions.notifyEvent(NotificationType.RefreshMelted)
subscriptions.notifyEvent(NotificationType.RefreshMelted);
}
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() =>
useComponentState("taler://refund/asdasdas", {
prepareRefund: async () => ({
awaiting: Amounts.stringify(awaiting),
effectivePaid: 'EUR:2',
gone: 'EUR:0',
granted: Amounts.stringify(granted),
pending,
proposalId: '1',
info: {
contractTermsHash: '123',
merchant: {
name: 'the merchant name'
},
orderId: 'orderId1',
summary: 'the sumary'
}
} as PrepareRefundResult as any),
applyRefund: async () => ({}),
onUpdateNotification: subscriptions.saveSubscription,
} as any),
);
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() =>
useComponentState("taler://refund/asdasdas", {
prepareRefund: async () =>
({
awaiting: Amounts.stringify(awaiting),
effectivePaid: "EUR:2",
gone: "EUR:0",
granted: Amounts.stringify(granted),
pending,
proposalId: "1",
info: {
contractTermsHash: "123",
merchant: {
name: "the merchant name",
},
orderId: "orderId1",
summary: "the sumary",
},
} as PrepareRefundResult as any),
applyRefund: async () => ({}),
onUpdateNotification: subscriptions.saveSubscription,
} as any),
);
{
const { status, hook } = getLastResultOrThrow()
expect(status).equals('loading')
const { status, hook } = getLastResultOrThrow();
expect(status).equals("loading");
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();
expect(state.merchantName).eq('the merchant name');
expect(state.merchantName).eq("the merchant name");
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)
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();
expect(state.merchantName).eq('the merchant name');
expect(state.merchantName).eq("the merchant name");
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)
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();
expect(state.merchantName).eq('the merchant name');
expect(state.merchantName).eq("the merchant name");
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,64 +26,67 @@ import { useComponentState } from "./Tip.jsx";
describe("Tip CTA states", () => {
it("should tell the user that the URI is missing", async () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() =>
useComponentState(undefined, {
prepareTip: async () => ({}),
acceptTip: async () => ({})
} as any),
);
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() =>
useComponentState(undefined, {
prepareTip: async () => ({}),
acceptTip: async () => ({}),
} as any),
);
{
const { status, hook } = getLastResultOrThrow()
expect(status).equals('loading')
const { status, hook } = getLastResultOrThrow();
expect(status).equals("loading");
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.hasError) expect.fail();
if (hook.operational) expect.fail();
expect(hook.message).eq("ERROR_NO-URI-FOR-TIP");
}
await assertNoPendingUpdate()
await assertNoPendingUpdate();
});
it("should be ready for accepting the tip", async () => {
let tipAccepted = false;
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() =>
useComponentState("taler://tip/asd", {
prepareTip: async () => ({
accepted: tipAccepted,
exchangeBaseUrl: "exchange url",
merchantBaseUrl: "merchant url",
tipAmountEffective: "EUR:1",
walletTipId: "tip_id",
} as PrepareTipResult as any),
acceptTip: async () => {
tipAccepted = true
}
} as any),
);
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() =>
useComponentState("taler://tip/asd", {
prepareTip: async () =>
({
accepted: tipAccepted,
exchangeBaseUrl: "exchange url",
merchantBaseUrl: "merchant url",
tipAmountEffective: "EUR:1",
walletTipId: "tip_id",
} as PrepareTipResult as any),
acceptTip: async () => {
tipAccepted = true;
},
} as any),
);
{
const { status, hook } = getLastResultOrThrow()
expect(status).equals('loading')
const { status, hook } = getLastResultOrThrow();
expect(status).equals("loading");
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();
expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:1"));
expect(state.merchantBaseUrl).eq("merchant url");
@ -93,45 +96,46 @@ describe("Tip CTA states", () => {
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();
expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:1"));
expect(state.merchantBaseUrl).eq("merchant url");
expect(state.exchangeBaseUrl).eq("exchange url");
}
await assertNoPendingUpdate()
await assertNoPendingUpdate();
});
it("should be ignored after clicking the ignore button", async () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() =>
useComponentState("taler://tip/asd", {
prepareTip: async () => ({
exchangeBaseUrl: "exchange url",
merchantBaseUrl: "merchant url",
tipAmountEffective: "EUR:1",
walletTipId: "tip_id",
} as PrepareTipResult as any),
acceptTip: async () => ({})
} as any),
);
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() =>
useComponentState("taler://tip/asd", {
prepareTip: async () =>
({
exchangeBaseUrl: "exchange url",
merchantBaseUrl: "merchant url",
tipAmountEffective: "EUR:1",
walletTipId: "tip_id",
} as PrepareTipResult as any),
acceptTip: async () => ({}),
} as any),
);
{
const { status, hook } = getLastResultOrThrow()
expect(status).equals('loading')
const { status, hook } = getLastResultOrThrow();
expect(status).equals("loading");
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();
expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:1"));
expect(state.merchantBaseUrl).eq("merchant url");
@ -141,52 +145,49 @@ describe("Tip CTA states", () => {
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();
}
await assertNoPendingUpdate()
await assertNoPendingUpdate();
});
it("should render accepted if the tip has been used previously", async () => {
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() =>
useComponentState("taler://tip/asd", {
prepareTip: async () => ({
accepted: true,
exchangeBaseUrl: "exchange url",
merchantBaseUrl: "merchant url",
tipAmountEffective: "EUR:1",
walletTipId: "tip_id",
} as PrepareTipResult as any),
acceptTip: async () => ({})
} as any),
);
const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } =
mountHook(() =>
useComponentState("taler://tip/asd", {
prepareTip: async () =>
({
accepted: true,
exchangeBaseUrl: "exchange url",
merchantBaseUrl: "merchant url",
tipAmountEffective: "EUR:1",
walletTipId: "tip_id",
} as PrepareTipResult as any),
acceptTip: async () => ({}),
} as any),
);
{
const { status, hook } = getLastResultOrThrow()
expect(status).equals('loading')
const { status, hook } = getLastResultOrThrow();
expect(status).equals("loading");
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();
expect(state.amount).deep.eq(Amounts.parseOrThrow("EUR:1"));
expect(state.merchantBaseUrl).eq("merchant url");
expect(state.exchangeBaseUrl).eq("exchange url");
}
await assertNoPendingUpdate()
await assertNoPendingUpdate();
});
});
});

View File

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

View File

@ -13,7 +13,7 @@
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/>
*/
declare module "*.jpeg" {
declare module "*.jpeg" {
const content: any;
export default content;
}

View File

@ -13,9 +13,7 @@
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/>
*/
import {
NotificationType, TalerErrorDetail
} from "@gnu-taler/taler-util";
import { NotificationType, TalerErrorDetail } from "@gnu-taler/taler-util";
import { TalerError } from "@gnu-taler/taler-wallet-core";
import { useEffect, useMemo, useState } from "preact/hooks";
import * as wxApi from "../wxApi.js";
@ -44,19 +42,23 @@ interface WithRetry {
}
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>(
fn: () => Promise<T | false>,
deps?: any[],
): HookResponseWithRetry<T> {
const [result, setHookResponse] = useState<HookResponse<T>>(undefined);
const args = useMemo(() => ({
fn
// eslint-disable-next-line react-hooks/exhaustive-deps
}), deps || [])
const args = useMemo(
() => ({
fn,
// eslint-disable-next-line react-hooks/exhaustive-deps
}),
deps || [],
);
async function doAsync(): Promise<void> {
try {

View File

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

View File

@ -17,12 +17,14 @@
import { useNotNullLocalStorage } from "./useLocalStorage.js";
function getBrowserLang(): string | undefined {
if (window.navigator.languages) return window.navigator.languages[0]
if (window.navigator.language) return window.navigator.language
if (window.navigator.languages) return window.navigator.languages[0];
if (window.navigator.language) return window.navigator.language;
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);
return useNotNullLocalStorage("lang-preference", defaultLang);
}

View File

@ -25,13 +25,13 @@ export function useLocalStorage(
key: string,
initialValue?: string,
): [string | undefined, StateUpdater<string | undefined>] {
const [storedValue, setStoredValue] = useState<string | undefined>(():
| string
| undefined => {
return typeof window !== "undefined"
? window.localStorage.getItem(key) || initialValue
: initialValue;
});
const [storedValue, setStoredValue] = useState<string | undefined>(
(): string | undefined => {
return typeof window !== "undefined"
? window.localStorage.getItem(key) || initialValue
: initialValue;
},
);
const setValue = (
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
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 { IoCProviderForTesting } from "../context/iocContext.js";
import { h, VNode } from "preact";
import { expect } from "chai";
describe('useTalerActionURL hook', () => {
it('should be set url to undefined when dismiss', async () => {
describe("useTalerActionURL hook", () => {
it("should be set url to undefined when dismiss", async () => {
const ctx = ({ children }: { children: any }): VNode => {
return h(IoCProviderForTesting, {
value: {
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;
}
await waitNextUpdate("waiting for useEffect")
await waitNextUpdate("waiting for useEffect");
{
const [url, setDismissed] = getLastResultOrThrow()
const [url, setDismissed] = getLastResultOrThrow();
expect(url).equals("asd");
setDismissed(true)
setDismissed(true);
}
await waitNextUpdate("after dismiss")
await waitNextUpdate("after dismiss");
{
const [url] = getLastResultOrThrow()
if (url !== undefined) throw Error('invalid')
const [url] = getLastResultOrThrow();
if (url !== undefined) throw Error("invalid");
expect(url).undefined;
}
await assertNoPendingUpdate()
})
})
await assertNoPendingUpdate();
});
});

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,4 +1,4 @@
import { expect } from 'chai';
import { expect } from "chai";
import {
recomposeColor,
hexToRgb,
@ -11,295 +11,308 @@ import {
getContrastRatio,
getLuminance,
lighten,
} from './manipulation.js';
} from "./manipulation.js";
describe('utils/colorManipulator', () => {
describe('recomposeColor', () => {
it('converts a decomposed rgb color object to a string` ', () => {
describe("utils/colorManipulator", () => {
describe("recomposeColor", () => {
it("converts a decomposed rgb color object to a string` ", () => {
expect(
recomposeColor({
type: 'rgb',
type: "rgb",
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(
recomposeColor({
type: 'rgba',
type: "rgba",
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(
recomposeColor({
type: 'hsl',
type: "hsl",
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(
recomposeColor({
type: 'hsla',
type: "hsla",
values: [100, 50, 25, 0.5],
}),
).to.equal('hsla(100, 50%, 25%, 0.5)');
).to.equal("hsla(100, 50%, 25%, 0.5)");
});
});
describe('hexToRgb', () => {
it('converts a short hex color to an rgb color` ', () => {
expect(hexToRgb('#9f3')).to.equal('rgb(153, 255, 51)');
describe("hexToRgb", () => {
it("converts a short hex color to an rgb color` ", () => {
expect(hexToRgb("#9f3")).to.equal("rgb(153, 255, 51)");
});
it('converts a long hex color to an rgb color` ', () => {
expect(hexToRgb('#a94fd3')).to.equal('rgb(169, 79, 211)');
it("converts a long hex color to an rgb color` ", () => {
expect(hexToRgb("#a94fd3")).to.equal("rgb(169, 79, 211)");
});
it('converts a long alpha hex color to an argb color` ', () => {
expect(hexToRgb('#111111f8')).to.equal('rgba(17, 17, 17, 0.973)');
it("converts a long alpha hex color to an argb color` ", () => {
expect(hexToRgb("#111111f8")).to.equal("rgba(17, 17, 17, 0.973)");
});
});
describe('rgbToHex', () => {
it('converts an rgb color to a hex color` ', () => {
expect(rgbToHex('rgb(169, 79, 211)')).to.equal('#a94fd3');
describe("rgbToHex", () => {
it("converts an rgb color to a hex color` ", () => {
expect(rgbToHex("rgb(169, 79, 211)")).to.equal("#a94fd3");
});
it('converts an rgba color to a hex color` ', () => {
expect(rgbToHex('rgba(169, 79, 211, 1)')).to.equal('#a94fd3ff');
it("converts an rgba color to a hex color` ", () => {
expect(rgbToHex("rgba(169, 79, 211, 1)")).to.equal("#a94fd3ff");
});
it('idempotent', () => {
expect(rgbToHex('#A94FD3')).to.equal('#A94FD3');
it("idempotent", () => {
expect(rgbToHex("#A94FD3")).to.equal("#A94FD3");
});
});
describe('hslToRgb', () => {
it('converts an hsl color to an rgb color` ', () => {
expect(hslToRgb('hsl(281, 60%, 57%)')).to.equal('rgb(169, 80, 211)');
describe("hslToRgb", () => {
it("converts an hsl color to an rgb color` ", () => {
expect(hslToRgb("hsl(281, 60%, 57%)")).to.equal("rgb(169, 80, 211)");
});
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)');
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)",
);
});
it('allow to convert values only', () => {
expect(hslToRgb('hsl(281, 60%, 57%)')).to.equal('rgb(169, 80, 211)');
it("allow to convert values only", () => {
expect(hslToRgb("hsl(281, 60%, 57%)")).to.equal("rgb(169, 80, 211)");
});
});
describe('decomposeColor', () => {
it('converts an rgb color string to an object with `type` and `value` keys', () => {
const { type, values } = decomposeColor('rgb(255, 255, 255)');
expect(type).to.equal('rgb');
describe("decomposeColor", () => {
it("converts an rgb color string to an object with `type` and `value` keys", () => {
const { type, values } = decomposeColor("rgb(255, 255, 255)");
expect(type).to.equal("rgb");
expect(values).to.deep.equal([255, 255, 255]);
});
it('converts an rgba color string to an object with `type` and `value` keys', () => {
const { type, values } = decomposeColor('rgba(255, 255, 255, 0.5)');
expect(type).to.equal('rgba');
it("converts an rgba color string to an object with `type` and `value` keys", () => {
const { type, values } = decomposeColor("rgba(255, 255, 255, 0.5)");
expect(type).to.equal("rgba");
expect(values).to.deep.equal([255, 255, 255, 0.5]);
});
it('converts an hsl color string to an object with `type` and `value` keys', () => {
const { type, values } = decomposeColor('hsl(100, 50%, 25%)');
expect(type).to.equal('hsl');
it("converts an hsl color string to an object with `type` and `value` keys", () => {
const { type, values } = decomposeColor("hsl(100, 50%, 25%)");
expect(type).to.equal("hsl");
expect(values).to.deep.equal([100, 50, 25]);
});
it('converts an hsla color string to an object with `type` and `value` keys', () => {
const { type, values } = decomposeColor('hsla(100, 50%, 25%, 0.5)');
expect(type).to.equal('hsla');
it("converts an hsla color string to an object with `type` and `value` keys", () => {
const { type, values } = decomposeColor("hsla(100, 50%, 25%, 0.5)");
expect(type).to.equal("hsla");
expect(values).to.deep.equal([100, 50, 25, 0.5]);
});
it('converts rgba hex', () => {
const decomposed = decomposeColor('#111111f8');
it("converts rgba hex", () => {
const decomposed = decomposeColor("#111111f8");
expect(decomposed).to.deep.equal({
type: 'rgba',
type: "rgba",
colorSpace: undefined,
values: [17, 17, 17, 0.973],
});
});
});
describe('getContrastRatio', () => {
it('returns a ratio for black : white', () => {
expect(getContrastRatio('#000', '#FFF')).to.equal(21);
describe("getContrastRatio", () => {
it("returns a ratio for black : white", () => {
expect(getContrastRatio("#000", "#FFF")).to.equal(21);
});
it('returns a ratio for black : black', () => {
expect(getContrastRatio('#000', '#000')).to.equal(1);
it("returns a ratio for black : black", () => {
expect(getContrastRatio("#000", "#000")).to.equal(1);
});
it('returns a ratio for white : white', () => {
expect(getContrastRatio('#FFF', '#FFF')).to.equal(1);
it("returns a ratio for white : white", () => {
expect(getContrastRatio("#FFF", "#FFF")).to.equal(1);
});
it('returns a ratio for dark-grey : light-grey', () => {
expect(getContrastRatio('#707070', '#E5E5E5')).to.be.approximately(3.93, 0.01);
it("returns a ratio for dark-grey : light-grey", () => {
expect(getContrastRatio("#707070", "#E5E5E5")).to.be.approximately(
3.93,
0.01,
);
});
it('returns a ratio for black : light-grey', () => {
expect(getContrastRatio('#000', '#888')).to.be.approximately(5.92, 0.01);
it("returns a ratio for black : light-grey", () => {
expect(getContrastRatio("#000", "#888")).to.be.approximately(5.92, 0.01);
});
});
describe('getLuminance', () => {
it('returns a valid luminance for rgb white ', () => {
expect(getLuminance('rgba(255, 255, 255)')).to.equal(1);
expect(getLuminance('rgb(255, 255, 255)')).to.equal(1);
describe("getLuminance", () => {
it("returns a valid luminance for rgb white ", () => {
expect(getLuminance("rgba(255, 255, 255)")).to.equal(1);
expect(getLuminance("rgb(255, 255, 255)")).to.equal(1);
});
it('returns a valid luminance for rgb mid-grey', () => {
expect(getLuminance('rgba(127, 127, 127)')).to.equal(0.212);
expect(getLuminance('rgb(127, 127, 127)')).to.equal(0.212);
it("returns a valid luminance for rgb mid-grey", () => {
expect(getLuminance("rgba(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', () => {
expect(getLuminance('rgb(255, 127, 0)')).to.equal(0.364);
it("returns a valid luminance for an rgb color", () => {
expect(getLuminance("rgb(255, 127, 0)")).to.equal(0.364);
});
it('returns a valid luminance from an hsl color', () => {
expect(getLuminance('hsl(100, 100%, 50%)')).to.equal(0.735);
it("returns a valid luminance from an hsl color", () => {
expect(getLuminance("hsl(100, 100%, 50%)")).to.equal(0.735);
});
it('returns an equal luminance for the same color in different formats', () => {
const hsl = 'hsl(100, 100%, 50%)';
const rgb = 'rgb(85, 255, 0)';
it("returns an equal luminance for the same color in different formats", () => {
const hsl = "hsl(100, 100%, 50%)";
const rgb = "rgb(85, 255, 0)";
expect(getLuminance(hsl)).to.equal(getLuminance(rgb));
});
});
describe('emphasize', () => {
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));
describe("emphasize", () => {
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),
);
});
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));
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),
);
});
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));
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));
});
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));
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),
);
});
});
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("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)");
});
it('updates an rgba color with the alpha value provided', () => {
expect(alpha('rgba(255, 0, 0, 0.2)', 0.5)).to.equal('rgba(255, 0, 0, 0.5)');
it("updates an rgba color with the alpha value provided", () => {
expect(alpha("rgba(255, 0, 0, 0.2)", 0.5)).to.equal(
"rgba(255, 0, 0, 0.5)",
);
});
it('converts an hsl color to an hsla color with the value provided', () => {
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", () => {
expect(alpha("hsl(0, 100%, 50%)", 0.1)).to.equal(
"hsla(0, 100%, 50%, 0.1)",
);
});
it('updates an hsla color with the alpha value provided', () => {
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", () => {
expect(alpha("hsla(0, 100%, 50%, 0.2)", 0.5)).to.equal(
"hsla(0, 100%, 50%, 0.5)",
);
});
});
describe('darken', () => {
describe("darken", () => {
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', () => {
expect(darken('rgb(255, 255, 255)', 1)).to.equal('rgb(0, 0, 0)');
it("darkens rgb white to black when coefficient is 1", () => {
expect(darken("rgb(255, 255, 255)", 1)).to.equal("rgb(0, 0, 0)");
});
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)');
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)");
});
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)');
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)");
});
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)');
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)");
});
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)');
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)");
});
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', () => {
expect(darken('hsl(0, 100%, 50%)', 0.5)).to.equal('hsl(0, 100%, 25%)');
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%)");
});
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%", () => {
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", () => {
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', () => {
expect(lighten('rgb(0, 0, 0)', 1)).to.equal('rgb(255, 255, 255)');
it("lightens rgb black to white when coefficient is 1", () => {
expect(lighten("rgb(0, 0, 0)", 1)).to.equal("rgb(255, 255, 255)");
});
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)');
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)",
);
});
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)');
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)");
});
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)');
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)");
});
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)');
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)");
});
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', () => {
expect(lighten('hsl(0, 100%, 50%)', 0.5)).to.equal('hsl(0, 100%, 75%)');
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%)");
});
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%", () => {
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 ColorFormatWithAlpha = 'rgb' | 'hsl';
export type ColorFormatWithoutAlpha = 'rgba' | 'hsla';
export type ColorObject = ColorObjectWithAlpha | ColorObjectWithoutAlpha
export type ColorFormat = ColorFormatWithAlpha | ColorFormatWithoutAlpha;
export type ColorFormatWithAlpha = "rgb" | "hsl";
export type ColorFormatWithoutAlpha = "rgba" | "hsla";
export type ColorObject = ColorObjectWithAlpha | ColorObjectWithoutAlpha;
export interface ColorObjectWithAlpha {
type: ColorFormatWithAlpha;
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 {
type: ColorFormatWithoutAlpha;
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.
* @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 {
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);
if (colors && colors[0].length === 1) {
@ -48,12 +46,14 @@ export function hexToRgb(color: string): string {
}
return colors
? `rgb${colors.length === 4 ? 'a' : ''}(${colors
.map((n, index) => {
return index < 3 ? parseInt(n, 16) : Math.round((parseInt(n, 16) / 255) * 1000) / 1000;
})
.join(', ')})`
: '';
? `rgb${colors.length === 4 ? "a" : ""}(${colors
.map((n, index) => {
return index < 3
? parseInt(n, 16)
: Math.round((parseInt(n, 16) / 255) * 1000) / 1000;
})
.join(", ")})`
: "";
}
function intToHex(int: number): string {
@ -70,23 +70,42 @@ function intToHex(int: number): string {
*/
export function decomposeColor(color: string): ColorObject {
const colorSpace = undefined;
if (color.charAt(0) === '#') {
if (color.charAt(0) === "#") {
return decomposeColor(hexToRgb(color));
}
const marker = color.indexOf('(');
const marker = color.indexOf("(");
const type = color.substring(0, marker);
// if (type != 'rgba' && type != 'hsla' && type != 'rgb' && type != 'hsl') {
// }
const values = color.substring(marker + 1, color.length - 1).split(',')
if (type == 'rgb' || type == 'hsl') {
return { type, colorSpace, values: [parseFloat(values[0]), parseFloat(values[1]), parseFloat(values[2])] }
const values = color.substring(marker + 1, color.length - 1).split(",");
if (type == "rgb" || type == "hsl") {
return {
type,
colorSpace,
values: [
parseFloat(values[0]),
parseFloat(values[1]),
parseFloat(values[2]),
],
};
}
if (type == 'rgba' || type == 'hsla') {
return { type, colorSpace, values: [parseFloat(values[0]), parseFloat(values[1]), parseFloat(values[2]), parseFloat(values[3])] }
if (type == "rgba" || type == "hsla") {
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 valuesStr: string[] = [];
if (type.indexOf('rgb') !== -1) {
if (type.indexOf("rgb") !== -1) {
// 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));
} else if (type.indexOf('hsl') !== -1) {
valuesStr[0] = String(valuesNum[0])
valuesNum
.map((n, i) => (i < 3 ? parseInt(String(n), 10) : n))
.forEach((n, i) => (valuesStr[i] = String(n)));
} else if (type.indexOf("hsl") !== -1) {
valuesStr[0] = String(valuesNum[0]);
valuesStr[1] = `${valuesNum[1]}%`;
valuesStr[2] = `${valuesNum[2]}%`;
if (type === 'hsla') {
valuesStr[3] = String(valuesNum[3])
if (type === "hsla") {
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 {
// Idempotent
if (color.indexOf('#') === 0) {
if (color.indexOf("#") === 0) {
return 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 l = values[2] / 100;
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({
type: 'rgba', values: [
type: "rgba",
values: [
Math.round(f(0) * 255),
Math.round(f(8) * 255),
Math.round(f(4) * 255),
colorObj.values[3]
]
})
colorObj.values[3],
],
});
}
return recomposeColor({
type: 'rgb', values: [
type: "rgb",
values: [
Math.round(f(0) * 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 {
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) => {
val /= 255; // normalized
return val <= 0.03928 ? val / 12.92 : ((val + 0.055) / 1.055) ** 2.4;
}) as typeof rgb2;
// 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()
* @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 lumB = getLuminance(background);
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);
value = clamp(value);
if (colorObj.type === 'rgb' || colorObj.type === 'hsl') {
colorObj.type += 'a';
if (colorObj.type === "rgb" || colorObj.type === "hsl") {
colorObj.type += "a";
}
colorObj.values[3] = value;
@ -226,9 +261,12 @@ export function darken(color: string, coefficient: number): string {
const colorObj = decomposeColor(color);
coefficient = clamp(coefficient);
if (colorObj.type.indexOf('hsl') !== -1) {
if (colorObj.type.indexOf("hsl") !== -1) {
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) {
colorObj.values[i] *= 1 - coefficient;
}
@ -246,13 +284,13 @@ export function lighten(color: string, coefficient: number): string {
const colorObj = decomposeColor(color);
coefficient = clamp(coefficient);
if (colorObj.type.indexOf('hsl') !== -1) {
if (colorObj.type.indexOf("hsl") !== -1) {
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) {
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) {
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
*/
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;
list: Record<string, string>;
}

View File

@ -22,12 +22,11 @@ export interface Permissions {
*/
permissions?: string[] | undefined;
/**
* List of origin permissions. Anything listed here must be a subset of a
* host that appears in the optional_permissions list in the manifest.
*
* List of origin permissions. Anything listed here must be a subset of a
* host that appears in the optional_permissions list in the manifest.
*
*/
origins?: string[] | undefined;
}
/**
@ -38,8 +37,9 @@ export interface CrossBrowserPermissionsApi {
requestHostPermissions(): Promise<boolean>;
removeHostPermissions(): Promise<boolean>;
addPermissionsListener(callback: (p: Permissions, lastError?: string) => void): void;
addPermissionsListener(
callback: (p: Permissions, lastError?: string) => void,
): void;
}
export type MessageFromBackend = {
@ -57,12 +57,12 @@ export interface WalletVersion {
*/
export interface PlatformAPI {
/**
* Garantee that the
* Garantee that the
*/
keepAlive(cb: VoidFunction): void;
/**
* FIXME: should not be needed
*
*
* check if the platform is firefox
*/
isFirefox(): boolean;
@ -74,46 +74,46 @@ export interface PlatformAPI {
/**
* Backend API
*
*
* Register a callback to be called when the wallet is ready to start
* @param callback
* @param callback
*/
notifyWhenAppIsReady(callback: () => void): void;
/**
* Popup API
*
*
* Used when an TalerURI is found and open up from the popup UI.
* Closes the popup and open the URI into the wallet UI.
*
* @param talerUri
*
* @param talerUri
*/
openWalletURIFromPopup(talerUri: string): void;
/**
* Backend API
*
*
* Open a page into the wallet UI
* @param page
* @param page
*/
openWalletPage(page: string): void;
/**
* Popup API
*
*
* Open a page into the wallet UI and closed the popup
* @param page
* @param page
*/
openWalletPageFromPopup(page: string): void;
/**
* Backend API
*
*
* When a tab has been detected to have a Taler action the background process
* can use this function to redirect the tab to the wallet UI
*
* @param tabId
* @param page
*
* @param tabId
* @param page
*/
redirectTabToWalletPage(tabId: number, page: string): void;
@ -122,7 +122,6 @@ export interface PlatformAPI {
*/
getWalletVersion(): WalletVersion;
/**
* Backend API
*/
@ -134,7 +133,9 @@ export interface PlatformAPI {
/**
* Backend API
*/
registerTalerHeaderListener(onHeader: (tabId: number, url: string) => void): void;
registerTalerHeaderListener(
onHeader: (tabId: number, url: string) => void,
): void;
/**
* Frontend API
*/
@ -146,49 +147,60 @@ export interface PlatformAPI {
/**
* Backend API
*
* Check if background process run as service worker. This is used from the
*
* Check if background process run as service worker. This is used from the
* wallet use different http api and crypto worker.
*/
useServiceWorkerAsBackgroundProcess(): boolean;
/**
* Popup API
*
*
* Read the current tab html and try to find any Taler URI or QR code present.
*
*
* @return Taler URI if found
*/
findTalerUriInActiveTab(): Promise<string | undefined>;
/**
* Used from the frontend to send commands to the wallet
*
* @param operation
* @param payload
*
*
* @param operation
* @param payload
*
* @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
* @param listener
* @return function to unsubscribe the listener
* @param 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)
* and send a response back.
*
* @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
* @param message
* @param message
*/
sendMessageToAllChannels(message: MessageFromBackend): void;
}

View File

@ -15,8 +15,18 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { classifyTalerUri, CoreApiResponse, Logger, TalerUriType } from "@gnu-taler/taler-util";
import { CrossBrowserPermissionsApi, MessageFromBackend, Permissions, PlatformAPI } from "./api.js";
import {
classifyTalerUri,
CoreApiResponse,
Logger,
TalerUriType,
} from "@gnu-taler/taler-util";
import {
CrossBrowserPermissionsApi,
MessageFromBackend,
Permissions,
PlatformAPI,
} from "./api.js";
const api: PlatformAPI = {
isFirefox,
@ -39,7 +49,7 @@ const api: PlatformAPI = {
useServiceWorkerAsBackgroundProcess,
containsTalerHeaderListener,
keepAlive,
}
};
export default api;
@ -47,16 +57,15 @@ const logger = new Logger("chrome.ts");
function keepAlive(callback: any): void {
if (extensionIsManifestV3()) {
chrome.alarms.create("wallet-worker", { periodInMinutes: 1 })
chrome.alarms.create("wallet-worker", { periodInMinutes: 1 });
chrome.alarms.onAlarm.addListener((a) => {
logger.trace(`kee p alive alarm: ${a.name}`)
logger.trace(`kee p alive alarm: ${a.name}`);
// callback()
})
});
// } else {
}
callback();
}
function isFirefox(): boolean {
@ -66,34 +75,35 @@ function isFirefox(): boolean {
const hostPermissions = {
permissions: ["webRequest"],
origins: ["http://*/*", "https://*/*"],
}
};
export function containsHostPermissions(): Promise<boolean> {
return new Promise((res, rej) => {
chrome.permissions.contains(hostPermissions, (resp) => {
const le = chrome.runtime.lastError?.message
const le = chrome.runtime.lastError?.message;
if (le) {
rej(le)
rej(le);
}
res(resp)
})
})
res(resp);
});
});
}
export async function requestHostPermissions(): Promise<boolean> {
return new Promise((res, rej) => {
chrome.permissions.request(hostPermissions, (resp) => {
const le = chrome.runtime.lastError?.message
const le = chrome.runtime.lastError?.message;
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;
export function containsTalerHeaderListener(): boolean {
@ -128,57 +138,69 @@ export async function removeHostPermissions(): Promise<boolean> {
}
return new Promise((res, rej) => {
chrome.permissions.remove(hostPermissions, (resp) => {
const le = chrome.runtime.lastError?.message
const le = chrome.runtime.lastError?.message;
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) => {
const lastError = chrome.runtime.lastError?.message;
callback(perm, lastError)
})
callback(perm, lastError);
});
}
function getPermissionsApi(): CrossBrowserPermissionsApi {
return {
addPermissionsListener, containsHostPermissions, requestHostPermissions, removeHostPermissions
}
addPermissionsListener,
containsHostPermissions,
requestHostPermissions,
removeHostPermissions,
};
}
/**
*
*
* @param callback function to be called
*/
function notifyWhenAppIsReady(callback: () => void): void {
if (extensionIsManifestV3()) {
callback()
callback();
} else {
window.addEventListener("load", callback);
}
}
function openWalletURIFromPopup(talerUri: string): void {
const uriType = classifyTalerUri(talerUri);
let url: string | undefined = undefined;
switch (uriType) {
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;
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;
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;
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;
default:
logger.warn(
@ -187,56 +209,54 @@ function openWalletURIFromPopup(talerUri: string): void {
return;
}
chrome.tabs.create(
{ active: true, url, },
() => { window.close(); },
);
chrome.tabs.create({ active: true, url }, () => {
window.close();
});
}
function openWalletPage(page: string): void {
const url = chrome.runtime.getURL(`/static/wallet.html#${page}`)
chrome.tabs.create(
{ active: true, url, },
);
const url = chrome.runtime.getURL(`/static/wallet.html#${page}`);
chrome.tabs.create({ active: true, url });
}
function openWalletPageFromPopup(page: string): void {
const url = chrome.runtime.getURL(`/static/wallet.html#${page}`)
chrome.tabs.create(
{ active: true, url, },
() => { window.close(); },
);
const url = chrome.runtime.getURL(`/static/wallet.html#${page}`);
chrome.tabs.create({ active: true, url }, () => {
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) => {
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) => {
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;
})
})
});
});
}
let notificationPort: chrome.runtime.Port | undefined;
function listenToWalletBackground(listener: (m: any) => void): () => void {
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 {
if (notificationPort !== undefined) {
notificationPort.onMessage.removeListener(listener)
notificationPort.onMessage.removeListener(listener);
}
}
return removeListener
return removeListener;
}
const allPorts: chrome.runtime.Port[] = [];
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) => {
cb(m, s, c)
cb(m, s, c);
// keep the connection open
return true;
@ -278,13 +304,9 @@ function registerReloadOnNewVersion(): void {
logger.info("update available:", details);
chrome.runtime.reload();
});
}
function redirectTabToWalletPage(
tabId: number,
page: string,
): void {
function redirectTabToWalletPage(tabId: number, page: string): void {
const url = chrome.runtime.getURL(`/static/wallet.html#${page}`);
logger.trace("redirecting tabId: ", tabId, " to: ", url);
chrome.tabs.update(tabId, { url });
@ -300,7 +322,9 @@ function getWalletVersion(): WalletVersion {
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");
function headerListener(
@ -316,44 +340,45 @@ function registerTalerHeaderListener(callback: (tabId: number, url: string) => v
details.statusCode === 200
) {
const values = (details.responseHeaders || [])
.filter(h => h.name.toLowerCase() === 'taler')
.map(h => h.value)
.filter((value): value is string => !!value)
.filter((h) => h.name.toLowerCase() === "taler")
.map((h) => h.value)
.filter((value): value is string => !!value);
if (values.length > 0) {
callback(details.tabId, values[0])
callback(details.tabId, values[0]);
}
}
return;
}
const prevHeaderListener = currentHeaderListener;
getPermissionsApi().containsHostPermissions().then(result => {
//if there is a handler already, remove it
if (
prevHeaderListener &&
chrome?.webRequest?.onHeadersReceived?.hasListener(prevHeaderListener)
) {
chrome.webRequest.onHeadersReceived.removeListener(prevHeaderListener);
}
//if the result was positive, add the headerListener
if (result) {
const listener: chrome.webRequest.WebResponseHeadersEvent | undefined = chrome?.webRequest?.onHeadersReceived;
if (listener) {
listener.addListener(
headerListener,
{ urls: ["<all_urls>"] },
["responseHeaders"],
);
currentHeaderListener = headerListener;
getPermissionsApi()
.containsHostPermissions()
.then((result) => {
//if there is a handler already, remove it
if (
prevHeaderListener &&
chrome?.webRequest?.onHeadersReceived?.hasListener(prevHeaderListener)
) {
chrome.webRequest.onHeadersReceived.removeListener(prevHeaderListener);
}
}
//notify the browser about this change, this operation is expensive
chrome?.webRequest?.handlerBehaviorChanged(() => {
if (chrome.runtime.lastError) {
logger.error(JSON.stringify(chrome.runtime.lastError));
//if the result was positive, add the headerListener
if (result) {
const listener: chrome.webRequest.WebResponseHeadersEvent | undefined =
chrome?.webRequest?.onHeadersReceived;
if (listener) {
listener.addListener(headerListener, { urls: ["<all_urls>"] }, [
"responseHeaders",
]);
currentHeaderListener = headerListener;
}
}
//notify the browser about this change, this operation is expensive
chrome?.webRequest?.handlerBehaviorChanged(() => {
if (chrome.runtime.lastError) {
logger.error(JSON.stringify(chrome.runtime.lastError));
}
});
});
});
}
const alertIcons = {
@ -365,8 +390,8 @@ const alertIcons = {
"64": "/static/img/taler-alert-64.png",
"128": "/static/img/taler-alert-128.png",
"256": "/static/img/taler-alert-256.png",
"512": "/static/img/taler-alert-512.png"
}
"512": "/static/img/taler-alert-512.png",
};
const normalIcons = {
"16": "/static/img/taler-logo-16.png",
"19": "/static/img/taler-logo-19.png",
@ -376,70 +401,99 @@ const normalIcons = {
"64": "/static/img/taler-logo-64.png",
"128": "/static/img/taler-logo-128.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 {
if (extensionIsManifestV3()) {
chrome.action.setIcon({ path: normalIcons })
chrome.action.setIcon({ path: normalIcons });
} else {
chrome.browserAction.setIcon({ path: normalIcons })
chrome.browserAction.setIcon({ path: normalIcons });
}
}
function setAlertedIcon(): void {
if (extensionIsManifestV3()) {
chrome.action.setIcon({ path: alertIcons })
chrome.action.setIcon({ path: alertIcons });
} else {
chrome.browserAction.setIcon({ path: alertIcons })
chrome.browserAction.setIcon({ path: alertIcons });
}
}
interface OffscreenCanvasRenderingContext2D extends CanvasState, CanvasTransform, CanvasCompositing, CanvasImageSmoothing, CanvasFillStrokeStyles, CanvasShadowStyles, CanvasFilters, CanvasRect, CanvasDrawPath, CanvasUserInterface, CanvasText, CanvasDrawImage, CanvasImageData, CanvasPathDrawingStyles, CanvasTextDrawingStyles, CanvasPath {
interface OffscreenCanvasRenderingContext2D
extends CanvasState,
CanvasTransform,
CanvasCompositing,
CanvasImageSmoothing,
CanvasFillStrokeStyles,
CanvasShadowStyles,
CanvasFilters,
CanvasRect,
CanvasDrawPath,
CanvasUserInterface,
CanvasText,
CanvasDrawImage,
CanvasImageData,
CanvasPathDrawingStyles,
CanvasTextDrawingStyles,
CanvasPath {
readonly canvas: OffscreenCanvas;
}
declare const OffscreenCanvasRenderingContext2D: {
prototype: OffscreenCanvasRenderingContext2D;
new(): OffscreenCanvasRenderingContext2D;
}
new (): OffscreenCanvasRenderingContext2D;
};
interface OffscreenCanvas extends EventTarget {
width: number;
height: number;
getContext(contextId: "2d", contextAttributes?: CanvasRenderingContext2DSettings): OffscreenCanvasRenderingContext2D | null;
getContext(
contextId: "2d",
contextAttributes?: CanvasRenderingContext2DSettings,
): OffscreenCanvasRenderingContext2D | null;
}
declare const OffscreenCanvas: {
prototype: OffscreenCanvas;
new(width: number, height: number): OffscreenCanvas;
}
new (width: number, height: number): OffscreenCanvas;
};
function createCanvas(size: number): OffscreenCanvas {
if (extensionIsManifestV3()) {
return new OffscreenCanvas(size, size)
return new OffscreenCanvas(size, size);
} else {
const c = document.createElement("canvas")
const c = document.createElement("canvas");
c.height = size;
c.width = size;
return c;
}
}
async function createImage(size: number, file: string): Promise<ImageData> {
const r = await fetch(file)
const b = await r.blob()
const image = await createImageBitmap(b)
const r = await fetch(file);
const b = await r.blob();
const image = await createImageBitmap(b);
const canvas = createCanvas(size);
const canvasContext = canvas.getContext('2d')!;
const canvasContext = canvas.getContext("2d")!;
canvasContext.clearRect(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;
}
async function registerIconChangeOnTalerContent(): Promise<void> {
const imgs = await Promise.all(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 })
const imgs = await Promise.all(
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) {
// using declarative content does not need host permission
@ -447,49 +501,54 @@ async function registerIconChangeOnTalerContent(): Promise<void> {
const secureTalerUrlLookup = {
conditions: [
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 = {
conditions: [
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.addRules([secureTalerUrlLookup, inSecureTalerUrlLookup]);
chrome.declarativeContent.onPageChanged.addRules([
secureTalerUrlLookup,
inSecureTalerUrlLookup,
]);
});
return;
}
//this browser doesn't have declarativeContent
//we need host_permission and we will check the content for changing the icon
chrome.tabs.onUpdated.addListener(async (tabId, info: chrome.tabs.TabChangeInfo) => {
if (tabId < 0) return;
logger.info("tab updated", tabId, info);
if (info.status !== "complete") return;
const uri = await findTalerUriInTab(tabId);
if (uri) {
setAlertedIcon()
} else {
setNormalIcon()
}
});
chrome.tabs.onActivated.addListener(async ({ tabId }: chrome.tabs.TabActiveInfo) => {
if (tabId < 0) return;
const uri = await findTalerUriInTab(tabId);
if (uri) {
setAlertedIcon()
} else {
setNormalIcon()
}
})
chrome.tabs.onUpdated.addListener(
async (tabId, info: chrome.tabs.TabChangeInfo) => {
if (tabId < 0) return;
logger.info("tab updated", tabId, info);
if (info.status !== "complete") return;
const uri = await findTalerUriInTab(tabId);
if (uri) {
setAlertedIcon();
} else {
setNormalIcon();
}
},
);
chrome.tabs.onActivated.addListener(
async ({ tabId }: chrome.tabs.TabActiveInfo) => {
if (tabId < 0) return;
const uri = await findTalerUriInTab(tabId);
if (uri) {
setAlertedIcon();
} else {
setNormalIcon();
}
},
);
}
function registerOnInstalled(callback: () => void): void {
@ -498,27 +557,27 @@ function registerOnInstalled(callback: () => void): void {
chrome.runtime.onInstalled.addListener(async (details) => {
logger.info(`onInstalled with reason: "${details.reason}"`);
if (details.reason === chrome.runtime.OnInstalledReason.INSTALL) {
callback()
callback();
}
registerIconChangeOnTalerContent()
registerIconChangeOnTalerContent();
});
}
function extensionIsManifestV3(): boolean {
return chrome.runtime.getManifest().manifest_version === 3
return chrome.runtime.getManifest().manifest_version === 3;
}
function useServiceWorkerAsBackgroundProcess(): boolean {
return extensionIsManifestV3()
return extensionIsManifestV3();
}
function searchForTalerLinks(): string | undefined {
let found;
found = document.querySelector("a[href^='taler://'")
if (found) return found.toString()
found = document.querySelector("a[href^='taler+http://'")
if (found) return found.toString()
return undefined
found = document.querySelector("a[href^='taler://'");
if (found) return found.toString();
found = document.querySelector("a[href^='taler+http://'");
if (found) return found.toString();
return undefined;
}
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) => {
chrome.tabs.query(queryOptions, (tabs) => {
if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError)
reject(chrome.runtime.lastError);
return;
}
resolve(tabs[0])
resolve(tabs[0]);
});
})
});
}
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({
target: { tabId, allFrames: true },
func: searchForTalerLinks,
args: []
})
return res[0].result
args: [],
});
return res[0].result;
} catch (e) {
return;
}
} else {
return new Promise((resolve, reject) => {
//manifest v2
chrome.tabs.executeScript(tabId,
chrome.tabs.executeScript(
tabId,
{
code: `
(() => {
@ -576,6 +636,5 @@ async function findTalerUriInTab(tabId: number): Promise<string | undefined> {
async function findTalerUriInActiveTab(): Promise<string | undefined> {
const tab = await getCurrentTab();
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 { MessageFromBackend, PlatformAPI } from "./api.js";
const frames = ["popup", "wallet"]
const frames = ["popup", "wallet"];
const api: PlatformAPI = ({
const api: PlatformAPI = {
isFirefox: () => false,
keepAlive: (cb: VoidFunction) => cb(),
findTalerUriInActiveTab: async () => undefined,
containsTalerHeaderListener: () => { return true },
containsTalerHeaderListener: () => {
return true;
},
getPermissionsApi: () => ({
addPermissionsListener: () => undefined, containsHostPermissions: async () => true, removeHostPermissions: async () => false, requestHostPermissions: async () => false
addPermissionsListener: () => undefined,
containsHostPermissions: async () => true,
removeHostPermissions: async () => false,
requestHostPermissions: async () => false,
}),
getWalletVersion: () => ({
version: 'none'
version: "none",
}),
notifyWhenAppIsReady: (fn: () => void) => {
let total = frames.length
let total = frames.length;
function waitAndNotify(): void {
total--
total--;
if (total < 1) {
console.log('done')
fn()
console.log("done");
fn();
}
}
frames.forEach(f => {
const theFrame = window.frames[f as any]
if (theFrame.location.href === 'about:blank') {
waitAndNotify()
frames.forEach((f) => {
const theFrame = window.frames[f as any];
if (theFrame.location.href === "about:blank") {
waitAndNotify();
} else {
theFrame.addEventListener("load", waitAndNotify)
theFrame.addEventListener("load", waitAndNotify);
}
})
});
},
openWalletPage: (page: string) => {
window.frames['wallet' as any].location = `/wallet.html#${page}`
window.frames["wallet" as any].location = `/wallet.html#${page}`;
},
openWalletPageFromPopup: (page: string) => {
window.parent.frames['wallet' as any].location = `/wallet.html#${page}`
window.location.href = "about:blank"
window.parent.frames["wallet" as any].location = `/wallet.html#${page}`;
window.location.href = "about:blank";
},
openWalletURIFromPopup: (page: string) => {
alert('openWalletURIFromPopup not implemented yet')
alert("openWalletURIFromPopup not implemented yet");
},
redirectTabToWalletPage: (tabId: number, page: string) => {
alert('redirectTabToWalletPage not implemented yet')
alert("redirectTabToWalletPage not implemented yet");
},
registerAllIncomingConnections: () => undefined,
@ -70,91 +75,101 @@ const api: PlatformAPI = ({
useServiceWorkerAsBackgroundProcess: () => false,
listenToAllChannels: (fn: (m: any, s: any, c: (r: CoreApiResponse) => void) => void) => {
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) => {
if (event.source) {
const msg: IframeMessageResponse = {
type: "response",
header: { responseId: sender },
body: resp
listenToAllChannels: (
fn: (m: any, s: any, c: (r: CoreApiResponse) => void) => void,
) => {
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) => {
if (event.source) {
const msg: IframeMessageResponse = {
type: "response",
header: { responseId: sender },
body: resp,
};
window.parent.postMessage(msg);
}
window.parent.postMessage(msg)
}
})
})
});
},
);
},
sendMessageToAllChannels: (message: MessageFromBackend) => {
Array.from(window.frames).forEach(w => {
Array.from(window.frames).forEach((w) => {
try {
w.postMessage({
header: {}, body: message
header: {},
body: message,
});
} catch (e) {
console.error(e)
console.error(e);
}
})
});
},
listenToWalletBackground: (onNewMessage: (m: MessageFromBackend) => void) => {
function listener(event: MessageEvent<IframeMessageType>): void {
if (event.data.type !== 'notification') return
onNewMessage(event.data.body)
if (event.data.type !== "notification") return;
onNewMessage(event.data.body);
}
window.parent.addEventListener("message", listener)
window.parent.addEventListener("message", listener);
return () => {
window.parent.removeEventListener("message", listener)
}
window.parent.removeEventListener("message", listener);
};
},
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 = {
type: 'command',
type: "command",
header: { replyMe },
body: { operation, payload, id: "(none)" }
}
window.parent.postMessage(message)
body: { operation, payload, id: "(none)" },
};
window.parent.postMessage(message);
return new Promise((res, rej) => {
function listener(event: MessageEvent<IframeMessageType>): void {
if (event.data.type !== "response" || event.data.header.responseId !== replyMe) {
return
if (
event.data.type !== "response" ||
event.data.header.responseId !== replyMe
) {
return;
}
res(event.data.body)
window.parent.removeEventListener("message", listener)
res(event.data.body);
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 {
type: "notification";
header: Record<string, never>,
body: MessageFromBackend
header: Record<string, never>;
body: MessageFromBackend;
}
interface IframeMessageResponse {
type: "response";
header: {
responseId: string;
},
body: CoreApiResponse
};
body: CoreApiResponse;
}
interface IframeMessageCommand {
type: "command";
header: {
replyMe: string;
},
};
body: {
operation: any, id: string, payload: any
}
operation: any;
id: string;
payload: any;
};
}
export default api;

View File

@ -15,7 +15,11 @@
*/
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 = {
...chromePlatform,
@ -23,18 +27,17 @@ const api: PlatformAPI = {
getPermissionsApi,
notifyWhenAppIsReady,
redirectTabToWalletPage,
useServiceWorkerAsBackgroundProcess
useServiceWorkerAsBackgroundProcess,
};
export default api;
function isFirefox(): boolean {
return true
return true;
}
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 {
@ -42,33 +45,28 @@ function getPermissionsApi(): CrossBrowserPermissionsApi {
addPermissionsListener,
containsHostPermissions: chromeContains,
requestHostPermissions: chromeRequest,
removeHostPermissions: chromeRemove
}
removeHostPermissions: chromeRemove,
};
}
/**
*
*
* @param callback function to be called
*/
function notifyWhenAppIsReady(callback: () => void): void {
if (chrome.runtime && chrome.runtime.getManifest().manifest_version === 3) {
callback()
callback();
} else {
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}`);
console.log("redirecting tabId: ", tabId, " to: ", url);
chrome.tabs.update(tabId, { url, loadReplace: true } as any);
}
function useServiceWorkerAsBackgroundProcess(): false {
return false
return false;
}

View File

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

View File

@ -47,9 +47,9 @@ function testThisStory(st: any): any {
describe("render every storybook example", () => {
[popup, wallet, cta, mui, components].forEach(function testAll(st: any) {
if (Array.isArray(st.default)) {
st.default.forEach(testAll)
st.default.forEach(testAll);
} 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/>
*/
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";
// 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
options.requestAnimationFrame = (fn: () => void) => {
// console.log("RAF called")
return fn()
}
return fn();
};
export function createExample<Props>(
Component: FunctionalComponent<Props>,
@ -30,8 +38,8 @@ export function createExample<Props>(
): ComponentChildren {
//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
// check how we can build evaluatedProps in render time
const evaluatedProps = typeof props === "function" ? props() : props
// check how we can build evaluatedProps in render time
const evaluatedProps = typeof props === "function" ? props() : props;
const Render = (args: any): VNode => create(Component, args);
Render.args = evaluatedProps;
return Render;
@ -43,14 +51,22 @@ export function createExampleWithCustomContext<Props, ContextProps>(
ContextProvider: FunctionalComponent<ContextProps>,
contextProps: Partial<ContextProps>,
): ComponentChildren {
const evaluatedProps = typeof props === "function" ? props() : props
const evaluatedProps = typeof props === "function" ? props() : props;
const Render = (args: any): VNode => create(Component, args);
const WithContext = (args: any): VNode => create(ContextProvider, { ...contextProps, children: [Render(args)] } as any);
WithContext.args = evaluatedProps
return WithContext
const WithContext = (args: any): VNode =>
create(ContextProvider, {
...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);" });
}
@ -74,53 +90,59 @@ interface Mounted<T> {
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 } = {
// current: null
// }
let lastResult: T | Error | null = null;
const listener: Array<() => void> = []
const listener: Array<() => void> = [];
// component that's going to hold the hook
function Component(): VNode {
try {
lastResult = callback()
lastResult = callback();
} catch (e) {
if (e instanceof Error) {
lastResult = e
lastResult = e;
} 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
listener.splice(0, listener.length).forEach(cb => cb())
return create(Fragment, {})
listener.splice(0, listener.length).forEach((cb) => cb());
return create(Fragment, {});
}
// 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
async function waitNextUpdate(_label = ""): Promise<void> {
if (_label) _label = `. label: "${_label}"`
if (_label) _label = `. label: "${_label}"`;
await new Promise((res, rej) => {
const tid = setTimeout(() => {
rej(Error(`waiting for an update but the hook didn't make one${_label}`))
}, 100)
rej(
Error(`waiting for an update but the hook didn't make one${_label}`),
);
}, 100);
listener.push(() => {
clearTimeout(tid)
res(undefined)
})
})
clearTimeout(tid);
res(undefined);
});
});
}
const customElement = {} as Element
const customElement = {} as Element;
const parentElement = isNode ? customElement : document.createElement("div");
if (!isNode) {
document.body.appendChild(parentElement);
@ -136,38 +158,44 @@ export function mountHook<T>(callback: () => T, Context?: ({ children }: { child
}
function getLastResult(): T | Error | null {
const copy = lastResult
lastResult = null
const copy = lastResult;
lastResult = null;
return copy;
}
function getLastResultOrThrow(): T {
const r = getLastResult()
const r = getLastResult();
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;
}
async function assertNoPendingUpdate(): Promise<void> {
await new Promise((res, rej) => {
const tid = setTimeout(() => {
res(undefined)
}, 10)
res(undefined);
}, 10);
listener.push(() => {
clearTimeout(tid)
rej(Error(`Expecting no pending result but the hook got updated.
clearTimeout(tid);
rej(
Error(`Expecting no pending result but the hook got updated.
If the update was not intended you need to check the hook dependencies
(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()
if (r) throw Error(`There are still pending results.
const r = getLastResult();
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`);
}
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/>
*/
import { AmountJson, Amounts, GetExchangeTosResult } from "@gnu-taler/taler-util";
import {
AmountJson,
Amounts,
GetExchangeTosResult,
} from "@gnu-taler/taler-util";
function getJsonIfOk(r: Response): Promise<any> {
if (r.ok) {
@ -27,15 +30,13 @@ function getJsonIfOk(r: Response): Promise<any> {
}
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>(
url: string,
): Promise<T> {
export async function queryToSlashConfig<T>(url: string): Promise<T> {
return fetch(new URL("config", url).href)
.catch(() => {
throw new Error(`Network error`);
@ -46,25 +47,27 @@ export async function queryToSlashConfig<T>(
function timeout<T>(ms: number, promise: Promise<T>): Promise<T> {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error(`Timeout: the query took longer than ${Math.floor(ms / 1000)} secs`))
}, ms)
reject(
new Error(
`Timeout: the query took longer than ${Math.floor(ms / 1000)} secs`,
),
);
}, ms);
promise
.then(value => {
clearTimeout(timer)
resolve(value)
.then((value) => {
clearTimeout(timer);
resolve(value);
})
.catch(reason => {
clearTimeout(timer)
reject(reason)
})
})
.catch((reason) => {
clearTimeout(timer);
reject(reason);
});
});
}
export async function queryToSlashKeys<T>(
url: string,
): Promise<T> {
const endpoint = new URL("keys", url)
export async function queryToSlashKeys<T>(url: string): Promise<T> {
const endpoint = new URL("keys", url);
endpoint.searchParams.set("cacheBreaker", new Date().getTime() + "");
const query = fetch(endpoint.href)
@ -73,29 +76,37 @@ export async function queryToSlashKeys<T>(
})
.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(
tos.contentType,
tos.content,
);
const status: TermsStatus = buildTermsOfServiceStatus(tos.content, tos.acceptedEtag, tos.currentEtag);
return { content, status, version: tos.currentEtag }
const status: TermsStatus = buildTermsOfServiceStatus(
tos.content,
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
? "notfound"
: !acceptedVersion
? "new"
: acceptedVersion !== currentVersion
? "changed"
: "accepted";
? "new"
: acceptedVersion !== currentVersion
? "changed"
: "accepted";
}
function parseTermsOfServiceContent(

View File

@ -25,15 +25,13 @@ import { SelectFieldHandler, TextFieldHandler } from "../mui/handlers.js";
import { mountHook } from "../test-utils.js";
import { useComponentState } from "./CreateManualWithdraw.js";
const exchangeListWithARSandUSD = {
"url1": "USD",
"url2": "ARS",
"url3": "ARS",
url1: "USD",
url2: "ARS",
url3: "ARS",
};
const exchangeListEmpty = {
};
const exchangeListEmpty = {};
describe("CreateManualWithdraw states", () => {
it("should set noExchangeFound when exchange list is empty", () => {
@ -41,9 +39,9 @@ describe("CreateManualWithdraw states", () => {
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", () => {
@ -51,20 +49,19 @@ describe("CreateManualWithdraw states", () => {
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", () => {
const { getLastResultOrThrow } = mountHook(() =>
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", () => {
@ -72,9 +69,9 @@ describe("CreateManualWithdraw states", () => {
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 () => {
@ -82,22 +79,20 @@ describe("CreateManualWithdraw states", () => {
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();
currency.onChange("USD")
currency.onChange("USD");
}
await waitNextUpdate()
await waitNextUpdate();
{
const { exchange } = getLastResultOrThrow()
expect(exchange.value).equal("url1")
const { exchange } = getLastResultOrThrow();
expect(exchange.value).equal("url1");
}
});
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(currency.value).equal("ARS")
expect(exchange.value).equal("url2");
expect(currency.value).equal("ARS");
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(currency.value).equal("USD")
expect(exchange.value).equal("url1");
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({
value: 12, fraction: 0, currency: "ARS"
})
value: 12,
fraction: 0,
currency: "ARS",
});
}
});
@ -154,67 +151,79 @@ describe("CreateManualWithdraw states", () => {
useComponentState(exchangeListWithARSandUSD, undefined, "ARS"),
);
await defaultTestForInputText(waitNextUpdate, () => getLastResultOrThrow().amount)
})
await defaultTestForInputText(
waitNextUpdate,
() => getLastResultOrThrow().amount,
);
});
it("should have an exchange selector ", async () => {
const { getLastResultOrThrow, waitNextUpdate } = mountHook(() =>
useComponentState(exchangeListWithARSandUSD, undefined, "ARS"),
);
await defaultTestForInputSelect(waitNextUpdate, () => getLastResultOrThrow().exchange)
})
await defaultTestForInputSelect(
waitNextUpdate,
() => getLastResultOrThrow().exchange,
);
});
it("should have a currency selector ", async () => {
const { getLastResultOrThrow, waitNextUpdate } = mountHook(() =>
useComponentState(exchangeListWithARSandUSD, undefined, "ARS"),
);
await defaultTestForInputSelect(waitNextUpdate, () => getLastResultOrThrow().currency)
})
await defaultTestForInputSelect(
waitNextUpdate,
() => getLastResultOrThrow().currency,
);
});
});
async function defaultTestForInputText(awaiter: () => Promise<void>, getField: () => TextFieldHandler): Promise<void> {
let nextValue = ''
async function defaultTestForInputText(
awaiter: () => Promise<void>,
getField: () => TextFieldHandler,
): Promise<void> {
let nextValue = "";
{
const field = getField()
const field = getField();
const initialValue = field.value;
nextValue = `${initialValue} something else`
field.onInput(nextValue)
nextValue = `${initialValue} something else`;
field.onInput(nextValue);
}
await awaiter()
await awaiter();
{
const field = getField()
expect(field.value).equal(nextValue)
const field = getField();
expect(field.value).equal(nextValue);
}
}
async function defaultTestForInputSelect(awaiter: () => Promise<void>, getField: () => SelectFieldHandler): Promise<void> {
let nextValue = ''
async function defaultTestForInputSelect(
awaiter: () => Promise<void>,
getField: () => SelectFieldHandler,
): Promise<void> {
let nextValue = "";
{
const field = getField();
const initialValue = field.value;
const keys = Object.keys(field.list)
const nextIdx = keys.indexOf(initialValue) + 1
const keys = Object.keys(field.list);
const nextIdx = keys.indexOf(initialValue) + 1;
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();
field.onChange(nextValue)
field.onChange(nextValue);
}
await awaiter()
await awaiter();
{
const field = getField();
expect(field.value).equal(nextValue)
expect(field.value).equal(nextValue);
}
}

View File

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

View File

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

View File

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

View File

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