summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-webextension/src/wxBackend.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-wallet-webextension/src/wxBackend.ts')
-rw-r--r--packages/taler-wallet-webextension/src/wxBackend.ts533
1 files changed, 297 insertions, 236 deletions
diff --git a/packages/taler-wallet-webextension/src/wxBackend.ts b/packages/taler-wallet-webextension/src/wxBackend.ts
index 28adfa037..008f80c57 100644
--- a/packages/taler-wallet-webextension/src/wxBackend.ts
+++ b/packages/taler-wallet-webextension/src/wxBackend.ts
@@ -24,32 +24,42 @@
* Imports.
*/
import {
- classifyTalerUri,
- CoreApiResponse,
- CoreApiResponseSuccess,
+ AbsoluteTime,
+ BalanceFlag,
+ LogLevel,
Logger,
+ NotificationType,
+ OpenedPromise,
+ SetTimeoutTimerAPI,
+ TalerError,
TalerErrorCode,
- TalerUriType,
- WalletDiagnostics,
+ TalerErrorDetail,
+ TransactionMajorState,
+ TransactionMinorState,
+ WalletNotification,
+ getErrorDetailFromException,
+ makeErrorDetail,
+ openPromise,
+ setGlobalLogLevelFromString,
+ setLogLevelFromString,
} from "@gnu-taler/taler-util";
+import { HttpRequestLibrary } from "@gnu-taler/taler-util/http";
import {
DbAccess,
+ SynchronousCryptoWorkerFactoryPlain,
+ Wallet,
+ WalletApiOperation,
+ WalletOperations,
+ WalletStoresV1,
deleteTalerDatabase,
exportDb,
importDb,
- makeErrorDetail,
- OpenedPromise,
- openPromise,
- openTalerDatabase,
- Wallet,
- WalletStoresV1,
} from "@gnu-taler/taler-wallet-core";
-import { SetTimeoutTimerAPI } from "@gnu-taler/taler-wallet-core";
-import { BrowserCryptoWorkerFactory } from "./browserCryptoWorkerFactory.js";
-import { BrowserHttpLib } from "./browserHttpLib.js";
-import { MessageFromBackend, platform } from "./platform/api.js";
-import { SynchronousCryptoWorkerFactory } from "./serviceWorkerCryptoWorkerFactory.js";
-import { ServiceWorkerHttpLib } from "./serviceWorkerHttpLib.js";
+import { MessageFromFrontend, MessageResponse } from "./platform/api.js";
+import { platform } from "./platform/background.js";
+import { ExtensionOperations } from "./taler-wallet-interaction-loader.js";
+import { BackgroundOperations, WalletEvent } from "./wxApi.js";
+import { BrowserFetchHttpLib } from "@gnu-taler/web-util/browser";
/**
* Currently active wallet instance. Might be unloaded and
@@ -61,260 +71,285 @@ let currentWallet: Wallet | undefined;
let currentDatabase: DbAccess<typeof WalletStoresV1> | undefined;
-/**
- * Last version of an outdated DB, if applicable.
- */
-let outdatedDbVersion: number | undefined;
-
const walletInit: OpenedPromise<void> = openPromise<void>();
const logger = new Logger("wxBackend.ts");
-async function getDiagnostics(): Promise<WalletDiagnostics> {
- const manifestData = platform.getWalletWebExVersion();
- const errors: string[] = [];
- let firefoxIdbProblem = false;
- let dbOutdated = false;
- try {
- await walletInit.promise;
- } catch (e) {
- errors.push("Error during wallet initialization: " + e);
- if (
- currentDatabase === undefined &&
- outdatedDbVersion === undefined &&
- platform.isFirefox()
- ) {
- firefoxIdbProblem = true;
- }
- }
- if (!currentWallet) {
- errors.push("Could not create wallet backend.");
+type BackendHandlerType = {
+ [Op in keyof BackgroundOperations]: (
+ req: BackgroundOperations[Op]["request"],
+ ) => Promise<BackgroundOperations[Op]["response"]>;
+};
+
+type ExtensionHandlerType = {
+ [Op in keyof ExtensionOperations]: (
+ req: ExtensionOperations[Op]["request"],
+ ) => Promise<ExtensionOperations[Op]["response"]>;
+};
+
+async function resetDb(): Promise<void> {
+ await deleteTalerDatabase(indexedDB as any);
+ await reinitWallet();
+}
+
+//FIXME: maybe circular buffer
+const notifications: WalletEvent[] = [];
+async function getNotifications(): Promise<WalletEvent[]> {
+ return notifications;
+}
+
+async function clearNotifications(): Promise<void> {
+ notifications.splice(0, notifications.length);
+}
+
+async function runGarbageCollector(): Promise<void> {
+ const dbBeforeGc = currentDatabase;
+ if (!dbBeforeGc) {
+ throw Error("no current db before running gc");
}
- if (!currentDatabase) {
- errors.push("Could not open database");
+ const dump = await exportDb(indexedDB as any);
+
+ await deleteTalerDatabase(indexedDB as any);
+ logger.info("cleaned");
+ await reinitWallet();
+ logger.info("init");
+
+ const dbAfterGc = currentDatabase;
+ if (!dbAfterGc) {
+ throw Error("no current db before running gc");
}
- if (outdatedDbVersion !== undefined) {
- errors.push(`Outdated DB version: ${outdatedDbVersion}`);
- dbOutdated = true;
+ await importDb(dbAfterGc.idbHandle(), dump);
+ logger.info("imported");
+}
+
+const extensionHandlers: ExtensionHandlerType = {
+ isAutoOpenEnabled,
+ isDomainTrusted,
+};
+
+async function isAutoOpenEnabled(): Promise<boolean> {
+ const settings = await platform.getSettingsFromStorage();
+ return settings.autoOpen === true;
+}
+
+async function isDomainTrusted(): Promise<boolean> {
+ const settings = await platform.getSettingsFromStorage();
+ return settings.injectTalerSupport === true;
+}
+
+const backendHandlers: BackendHandlerType = {
+ resetDb,
+ runGarbageCollector,
+ getNotifications,
+ clearNotifications,
+ reinitWallet,
+ setLoggingLevel,
+};
+
+async function setLoggingLevel({
+ tag,
+ level,
+}: {
+ tag?: string;
+ level: LogLevel;
+}): Promise<void> {
+ logger.info(`setting ${tag} to ${level}`);
+ if (!tag) {
+ setGlobalLogLevelFromString(level);
+ } else {
+ setLogLevelFromString(tag, level);
}
- const diagnostics: WalletDiagnostics = {
- walletManifestDisplayVersion: manifestData.version_name || "(undefined)",
- walletManifestVersion: manifestData.version,
- errors,
- firefoxIdbProblem,
- dbOutdated,
- };
- return diagnostics;
}
+let nextMessageIndex = 0;
-async function dispatch(
- req: any,
- sender: any,
- sendResponse: any,
-): Promise<void> {
- let r: CoreApiResponse;
-
- const wrapResponse = (result: unknown): CoreApiResponseSuccess => {
- return {
- type: "response",
- id: req.id,
- operation: req.operation,
- result,
- };
- };
+async function dispatch<
+ Op extends WalletOperations | BackgroundOperations | ExtensionOperations,
+>(req: MessageFromFrontend<Op> & { id: string }): Promise<MessageResponse> {
+ nextMessageIndex = (nextMessageIndex + 1) % (Number.MAX_SAFE_INTEGER - 100);
- try {
- switch (req.operation) {
- case "wxGetDiagnostics": {
- r = wrapResponse(await getDiagnostics());
- break;
- }
- case "reset-db": {
- await deleteTalerDatabase(indexedDB as any);
- r = wrapResponse(await reinitWallet());
- break;
+ switch (req.channel) {
+ case "background": {
+ const handler = backendHandlers[req.operation] as (req: any) => any;
+ if (!handler) {
+ return {
+ type: "error",
+ id: req.id,
+ operation: String(req.operation),
+ error: getErrorDetailFromException(
+ Error(`unknown background operation`),
+ ),
+ };
}
- case "run-gc": {
- logger.info("gc");
- const dump = await exportDb(currentDatabase!.idbHandle());
- await deleteTalerDatabase(indexedDB as any);
- logger.info("cleaned");
- await reinitWallet();
- logger.info("init");
- await importDb(currentDatabase!.idbHandle(), dump);
- logger.info("imported");
- r = wrapResponse({ result: true });
- break;
- }
- case "containsHeaderListener": {
- const res = await platform.containsTalerHeaderListener();
- r = wrapResponse({ newValue: res });
- break;
+ try {
+ const result = await handler(req.payload);
+ return {
+ type: "response",
+ id: req.id,
+ operation: String(req.operation),
+ result,
+ };
+ } catch (er) {
+ return {
+ type: "error",
+ id: req.id,
+ error: getErrorDetailFromException(er),
+ operation: String(req.operation),
+ };
}
- //FIXME: implement type checked api like WalletCoreApi
- case "toggleHeaderListener": {
- const newVal = req.payload.value;
- logger.trace("new extended permissions value", newVal);
- if (newVal) {
- platform.registerTalerHeaderListener(parseTalerUriAndRedirect);
- r = wrapResponse({ newValue: true });
- } else {
- const rem = await platform
- .getPermissionsApi()
- .removeHostPermissions();
- logger.trace("permissions removed:", rem);
- r = wrapResponse({ newVal: false });
- }
- break;
+ }
+ case "extension": {
+ const handler = extensionHandlers[req.operation] as (req: any) => any;
+ if (!handler) {
+ return {
+ type: "error",
+ id: req.id,
+ operation: String(req.operation),
+ error: getErrorDetailFromException(
+ Error(`unknown extension operation`),
+ ),
+ };
}
- default: {
- const w = currentWallet;
- if (!w) {
- r = {
- type: "error",
- id: req.id,
- operation: req.operation,
- error: makeErrorDetail(
- TalerErrorCode.WALLET_CORE_NOT_AVAILABLE,
- {},
- "wallet core not available",
- ),
- };
- break;
- }
- r = await w.handleCoreApiRequest(req.operation, req.id, req.payload);
- console.log("response received from wallet", r);
- break;
+ try {
+ const result = await handler(req.payload);
+ return {
+ type: "response",
+ id: req.id,
+ operation: String(req.operation),
+ result,
+ };
+ } catch (er) {
+ return {
+ type: "error",
+ id: req.id,
+ error: getErrorDetailFromException(er),
+ operation: String(req.operation),
+ };
}
}
+ case "wallet": {
+ const w = currentWallet;
+ if (!w) {
+ const lastError: TalerErrorDetail =
+ walletInit.lastError instanceof TalerError
+ ? walletInit.lastError.errorDetail
+ : undefined;
- sendResponse(r);
- } catch (e) {
- logger.error(`Error sending operation: ${req.operation}`, e);
- // might fail if tab disconnected
+ return {
+ type: "error",
+ id: req.id,
+ operation: req.operation,
+ error: makeErrorDetail(
+ TalerErrorCode.WALLET_CORE_NOT_AVAILABLE,
+ { lastError },
+ `wallet core not available${
+ !lastError ? "" : `,last error: ${lastError.hint}`
+ }`,
+ ),
+ };
+ }
+ //multiple client can create the same id, send the wallet an unique key
+ const newId = `${req.id}_${nextMessageIndex}`;
+ const resp = await w.handleCoreApiRequest(
+ req.operation,
+ newId,
+ req.payload,
+ );
+ //return to the client the original id
+ resp.id = req.id;
+ return resp;
+ }
}
+
+ const anyReq = req as any;
+ return {
+ type: "error",
+ id: anyReq.id,
+ operation: String(anyReq.operation),
+ error: getErrorDetailFromException(
+ Error(
+ `unknown channel ${anyReq.channel}, should be "background", "extension" or "wallet"`,
+ ),
+ ),
+ };
}
async function reinitWallet(): Promise<void> {
if (currentWallet) {
- currentWallet.stop();
+ await currentWallet.client.call(WalletApiOperation.Shutdown, {});
currentWallet = undefined;
}
currentDatabase = undefined;
// setBadgeText({ text: "" });
- try {
- currentDatabase = await openTalerDatabase(indexedDB as any, reinitWallet);
- } catch (e) {
- logger.error("could not open database", e);
- walletInit.reject(e);
- return;
- }
- let httpLib;
let cryptoWorker;
let timer;
+ const httpFactory = (): HttpRequestLibrary => {
+ return new BrowserFetchHttpLib({
+ // enableThrottling: false,
+ });
+ };
+
if (platform.useServiceWorkerAsBackgroundProcess()) {
- httpLib = new ServiceWorkerHttpLib();
- cryptoWorker = new SynchronousCryptoWorkerFactory();
+ cryptoWorker = new SynchronousCryptoWorkerFactoryPlain();
timer = new SetTimeoutTimerAPI();
} else {
- httpLib = new BrowserHttpLib();
// We could (should?) use the BrowserCryptoWorkerFactory here,
// but right now we don't, to have less platform differences.
// cryptoWorker = new BrowserCryptoWorkerFactory();
- cryptoWorker = new SynchronousCryptoWorkerFactory();
+ cryptoWorker = new SynchronousCryptoWorkerFactoryPlain();
timer = new SetTimeoutTimerAPI();
}
+ const settings = await platform.getSettingsFromStorage();
logger.info("Setting up wallet");
const wallet = await Wallet.create(
- currentDatabase,
- httpLib,
+ indexedDB as any,
+ httpFactory as any,
timer,
cryptoWorker,
);
try {
- await wallet.handleCoreApiRequest("initWallet", "native-init", {});
+ await wallet.handleCoreApiRequest("initWallet", "native-init", {
+ config: {
+ testing: {
+ emitObservabilityEvents: settings.showWalletActivity,
+ devModeActive: settings.advancedMode,
+ },
+ features: {
+ allowHttp: settings.walletAllowHttp,
+ },
+ },
+ });
} catch (e) {
logger.error("could not initialize wallet", e);
walletInit.reject(e);
return;
}
- wallet.addNotificationListener((x) => {
- const message: MessageFromBackend = { type: x.type };
- platform.sendMessageToAllChannels(message);
- });
+ wallet.addNotificationListener((message) => {
+ if (settings.showWalletActivity) {
+ notifications.push({
+ notification: message,
+ when: AbsoluteTime.now(),
+ });
+ }
+
+ processWalletNotification(message);
- platform.keepAlive(() => {
- return wallet.runTaskLoop().catch((e) => {
- logger.error("error during wallet task loop", e);
+ platform.sendMessageToAllChannels({
+ type: "wallet",
+ notification: message,
});
});
+
// Useful for debugging in the background page.
if (typeof window !== "undefined") {
(window as any).talerWallet = wallet;
}
currentWallet = wallet;
+ updateIconBasedOnBalance();
return walletInit.resolve();
}
-function parseTalerUriAndRedirect(tabId: number, talerUri: string): void {
- const uriType = classifyTalerUri(talerUri);
- switch (uriType) {
- case TalerUriType.TalerWithdraw:
- return platform.redirectTabToWalletPage(
- tabId,
- `/cta/withdraw?talerWithdrawUri=${talerUri}`,
- );
- case TalerUriType.TalerPay:
- return platform.redirectTabToWalletPage(
- tabId,
- `/cta/pay?talerPayUri=${talerUri}`,
- );
- case TalerUriType.TalerTip:
- return platform.redirectTabToWalletPage(
- tabId,
- `/cta/tip?talerTipUri=${talerUri}`,
- );
- case TalerUriType.TalerRefund:
- return platform.redirectTabToWalletPage(
- tabId,
- `/cta/refund?talerRefundUri=${talerUri}`,
- );
- case TalerUriType.TalerPayPull:
- return platform.redirectTabToWalletPage(
- tabId,
- `/cta/invoice/pay?talerPayPullUri=${talerUri}`,
- );
- case TalerUriType.TalerPayPush:
- return platform.redirectTabToWalletPage(
- tabId,
- `/cta/transfer/pickup?talerPayPushUri=${talerUri}`,
- );
- case TalerUriType.TalerRecovery:
- return platform.redirectTabToWalletPage(
- tabId,
- `/cta/transfer/recovery?talerBackupUri=${talerUri}`,
- );
- case TalerUriType.Unknown:
- logger.warn(
- `Response with HTTP 402 the Taler header but could not classify ${talerUri}`,
- );
- return;
- case TalerUriType.TalerDevExperiment:
- // FIXME: Implement!
- logger.warn("not implemented");
- return;
- default: {
- const error: never = uriType;
- logger.warn(
- `Response with HTTP 402 the Taler header "${error}", but header value is not a taler:// URI.`,
- );
- return;
- }
- }
-}
-
/**
* Main function to run for the WebExtension backend.
*
@@ -324,45 +359,71 @@ export async function wxMain(): Promise<void> {
logger.trace("starting");
const afterWalletIsInitialized = reinitWallet();
+ logger.trace("reload on new version");
platform.registerReloadOnNewVersion();
// Handlers for messages coming directly from the content
// script on the page
- platform.listenToAllChannels((message, sender, callback) => {
- afterWalletIsInitialized.then(() => {
- dispatch(message, sender, (response: CoreApiResponse) => {
- callback(response);
- });
- });
+ logger.trace("listen all channels");
+ platform.listenToAllChannels(async (message) => {
+ //wait until wallet is initialized
+ await afterWalletIsInitialized;
+ const result = await dispatch(message);
+ return result;
});
+ logger.trace("register all incoming connections");
platform.registerAllIncomingConnections();
+ logger.trace("redirect if first start");
try {
platform.registerOnInstalled(() => {
platform.openWalletPage("/welcome");
-
- //
- try {
- platform.registerTalerHeaderListener(parseTalerUriAndRedirect);
- } catch (e) {
- logger.error("could not register header listener", e);
- }
});
} catch (e) {
console.error(e);
}
+}
- // On platforms that support it, also listen to external
- // modification of permissions.
- platform.getPermissionsApi().addPermissionsListener((perm, lastError) => {
- if (lastError) {
- logger.error(
- `there was a problem trying to get permission ${perm}`,
- lastError,
- );
- return;
+async function updateIconBasedOnBalance() {
+ const balance = await currentWallet?.client.call(
+ WalletApiOperation.GetBalances,
+ {},
+ );
+ if (balance) {
+ let showAlert = false;
+ for (const b of balance.balances) {
+ if (b.flags.length > 0) {
+ console.log("b.flags", JSON.stringify(b.flags))
+ showAlert = true;
+ break;
+ }
}
- platform.registerTalerHeaderListener(parseTalerUriAndRedirect);
- });
+
+ if (showAlert) {
+ platform.setAlertedIcon();
+ } else {
+ platform.setNormalIcon();
+ }
+ }
+}
+
+/**
+ * All the actions triggered by notification that need to be
+ * run in the background.
+ *
+ * @param message
+ */
+async function processWalletNotification(message: WalletNotification) {
+ if (
+ message.type === NotificationType.TransactionStateTransition &&
+ (message.newTxState.minor === TransactionMinorState.KycRequired ||
+ message.oldTxState.minor === TransactionMinorState.KycRequired ||
+ message.newTxState.minor === TransactionMinorState.AmlRequired ||
+ message.oldTxState.minor === TransactionMinorState.AmlRequired ||
+ message.newTxState.minor === TransactionMinorState.BankConfirmTransfer ||
+ message.oldTxState.minor === TransactionMinorState.BankConfirmTransfer)
+ ) {
+ await updateIconBasedOnBalance();
+ }
}