summaryrefslogtreecommitdiffstats
path: root/toolkit/components/satchel/megalist/aggregator/datasources/BankCardDataSource.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/satchel/megalist/aggregator/datasources/BankCardDataSource.sys.mjs')
-rw-r--r--toolkit/components/satchel/megalist/aggregator/datasources/BankCardDataSource.sys.mjs339
1 files changed, 339 insertions, 0 deletions
diff --git a/toolkit/components/satchel/megalist/aggregator/datasources/BankCardDataSource.sys.mjs b/toolkit/components/satchel/megalist/aggregator/datasources/BankCardDataSource.sys.mjs
new file mode 100644
index 0000000000..06266a7979
--- /dev/null
+++ b/toolkit/components/satchel/megalist/aggregator/datasources/BankCardDataSource.sys.mjs
@@ -0,0 +1,339 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { DataSourceBase } from "resource://gre/modules/megalist/aggregator/datasources/DataSourceBase.sys.mjs";
+import { CreditCardRecord } from "resource://gre/modules/shared/CreditCardRecord.sys.mjs";
+import { formAutofillStorage } from "resource://autofill/FormAutofillStorage.sys.mjs";
+import { OSKeyStore } from "resource://gre/modules/OSKeyStore.sys.mjs";
+
+async function decryptCard(card) {
+ if (card["cc-number-encrypted"] && !card["cc-number-decrypted"]) {
+ try {
+ card["cc-number-decrypted"] = await OSKeyStore.decrypt(
+ card["cc-number-encrypted"],
+ false
+ );
+ card["cc-number"] = card["cc-number-decrypted"];
+ } catch (e) {
+ console.error(e);
+ }
+ }
+}
+
+async function updateCard(card, field, value) {
+ try {
+ await decryptCard(card);
+ const newCard = {
+ ...card,
+ [field]: value ?? "",
+ };
+ formAutofillStorage.INTERNAL_FIELDS.forEach(name => delete newCard[name]);
+ formAutofillStorage.creditCards.VALID_COMPUTED_FIELDS.forEach(
+ name => delete newCard[name]
+ );
+ delete newCard["cc-number-decrypted"];
+ CreditCardRecord.normalizeFields(newCard);
+
+ if (card.guid) {
+ await formAutofillStorage.creditCards.update(card.guid, newCard);
+ } else {
+ await formAutofillStorage.creditCards.add(newCard);
+ }
+ } catch (error) {
+ //todo
+ console.error("failed to modify credit card", error);
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * Data source for Bank Cards.
+ *
+ * Each card is represented by 3 lines: card number, expiration date and holder name.
+ *
+ * Protypes are used to reduce memory need because for different records
+ * similar lines will differ in values only.
+ */
+export class BankCardDataSource extends DataSourceBase {
+ #cardNumberPrototype;
+ #expirationPrototype;
+ #holderNamePrototype;
+ #cardsDisabledMessage;
+ #enabled;
+ #header;
+
+ constructor(...args) {
+ super(...args);
+ // Wait for Fluent to provide strings before loading data
+ this.formatMessages(
+ "payments-section-label",
+ "card-number-label",
+ "card-expiration-label",
+ "card-holder-label",
+ "command-copy",
+ "command-reveal",
+ "command-conceal",
+ "payments-disabled",
+ "command-delete",
+ "command-edit",
+ "payments-command-create"
+ ).then(
+ ([
+ headerLabel,
+ numberLabel,
+ expirationLabel,
+ holderLabel,
+ copyCommandLabel,
+ revealCommandLabel,
+ concealCommandLabel,
+ cardsDisabled,
+ deleteCommandLabel,
+ editCommandLabel,
+ cardsCreateCommandLabel,
+ ]) => {
+ const copyCommand = { id: "Copy", label: copyCommandLabel };
+ const editCommand = {
+ id: "Edit",
+ label: editCommandLabel,
+ verify: true,
+ };
+ const deleteCommand = {
+ id: "Delete",
+ label: deleteCommandLabel,
+ verify: true,
+ };
+ this.#cardsDisabledMessage = cardsDisabled;
+ this.#header = this.createHeaderLine(headerLabel);
+ this.#header.commands.push({
+ id: "Create",
+ label: cardsCreateCommandLabel,
+ });
+ this.#cardNumberPrototype = this.prototypeDataLine({
+ label: { value: numberLabel },
+ concealed: { value: true, writable: true },
+ start: { value: true },
+ value: {
+ async get() {
+ if (this.editingValue !== undefined) {
+ return this.editingValue;
+ }
+
+ if (this.concealed) {
+ return (
+ "••••••••" +
+ this.record["cc-number"].replaceAll("*", "").substr(-4)
+ );
+ }
+
+ await decryptCard(this.record);
+ return this.record["cc-number-decrypted"];
+ },
+ },
+ valueIcon: {
+ get() {
+ const typeToImage = {
+ amex: "third-party/cc-logo-amex.png",
+ cartebancaire: "third-party/cc-logo-cartebancaire.png",
+ diners: "third-party/cc-logo-diners.svg",
+ discover: "third-party/cc-logo-discover.png",
+ jcb: "third-party/cc-logo-jcb.svg",
+ mastercard: "third-party/cc-logo-mastercard.svg",
+ mir: "third-party/cc-logo-mir.svg",
+ unionpay: "third-party/cc-logo-unionpay.svg",
+ visa: "third-party/cc-logo-visa.svg",
+ };
+ return (
+ "chrome://formautofill/content/" +
+ (typeToImage[this.record["cc-type"]] ??
+ "icon-credit-card-generic.svg")
+ );
+ },
+ },
+ commands: {
+ get() {
+ const commands = [
+ { id: "Conceal", label: concealCommandLabel },
+ { ...copyCommand, verify: true },
+ editCommand,
+ "-",
+ deleteCommand,
+ ];
+ if (this.concealed) {
+ commands[0] = {
+ id: "Reveal",
+ label: revealCommandLabel,
+ verify: true,
+ };
+ }
+ return commands;
+ },
+ },
+ executeReveal: {
+ value() {
+ this.concealed = false;
+ this.refreshOnScreen();
+ },
+ },
+ executeConceal: {
+ value() {
+ this.concealed = true;
+ this.refreshOnScreen();
+ },
+ },
+ executeCopy: {
+ async value() {
+ await decryptCard(this.record);
+ this.copyToClipboard(this.record["cc-number-decrypted"]);
+ },
+ },
+ executeEdit: {
+ async value() {
+ await decryptCard(this.record);
+ this.editingValue = this.record["cc-number-decrypted"] ?? "";
+ this.refreshOnScreen();
+ },
+ },
+ executeSave: {
+ async value(value) {
+ if (updateCard(this.record, "cc-number", value)) {
+ this.executeCancel();
+ }
+ },
+ },
+ });
+ this.#expirationPrototype = this.prototypeDataLine({
+ label: { value: expirationLabel },
+ value: {
+ get() {
+ return `${this.record["cc-exp-month"]}/${this.record["cc-exp-year"]}`;
+ },
+ },
+ commands: {
+ value: [copyCommand, editCommand, "-", deleteCommand],
+ },
+ });
+ this.#holderNamePrototype = this.prototypeDataLine({
+ label: { value: holderLabel },
+ end: { value: true },
+ value: {
+ get() {
+ return this.editingValue ?? this.record["cc-name"];
+ },
+ },
+ commands: {
+ value: [copyCommand, editCommand, "-", deleteCommand],
+ },
+ executeEdit: {
+ value() {
+ this.editingValue = this.record["cc-name"] ?? "";
+ this.refreshOnScreen();
+ },
+ },
+ executeSave: {
+ async value(value) {
+ if (updateCard(this.record, "cc-name", value)) {
+ this.executeCancel();
+ }
+ },
+ },
+ });
+
+ Services.obs.addObserver(this, "formautofill-storage-changed");
+ Services.prefs.addObserver(
+ "extensions.formautofill.creditCards.enabled",
+ this
+ );
+ this.#reloadDataSource();
+ }
+ );
+ }
+
+ /**
+ * Enumerate all the lines provided by this data source.
+ *
+ * @param {string} searchText used to filter data
+ */
+ *enumerateLines(searchText) {
+ if (this.#enabled === undefined) {
+ // Async Fluent API makes it possible to have data source waiting
+ // for the localized strings, which can be detected by undefined in #enabled.
+ return;
+ }
+
+ yield this.#header;
+ if (this.#header.collapsed || !this.#enabled) {
+ return;
+ }
+
+ const stats = { count: 0, total: 0 };
+ searchText = searchText.toUpperCase();
+ yield* this.enumerateLinesForMatchingRecords(
+ searchText,
+ stats,
+ card =>
+ (card["cc-number-decrypted"] || card["cc-number"])
+ .toUpperCase()
+ .includes(searchText) ||
+ `${card["cc-exp-month"]}/${card["cc-exp-year"]}`
+ .toUpperCase()
+ .includes(searchText) ||
+ card["cc-name"].toUpperCase().includes(searchText)
+ );
+
+ this.formatMessages({
+ id:
+ stats.count == stats.total
+ ? "payments-count"
+ : "payments-filtered-count",
+ args: stats,
+ }).then(([headerLabel]) => {
+ this.#header.value = headerLabel;
+ });
+ }
+
+ /**
+ * Sync lines array with the actual data source.
+ * This function reads all cards from the storage, adds or updates lines and
+ * removes lines for the removed cards.
+ */
+ async #reloadDataSource() {
+ this.#enabled = Services.prefs.getBoolPref(
+ "extensions.formautofill.creditCards.enabled"
+ );
+ if (!this.#enabled) {
+ this.#reloadEmptyDataSource();
+ return;
+ }
+
+ await formAutofillStorage.initialize();
+ const cards = await formAutofillStorage.creditCards.getAll();
+ this.beforeReloadingDataSource();
+ cards.forEach(card => {
+ const lineId = `${card["cc-name"]}:${card.guid}`;
+
+ this.addOrUpdateLine(card, lineId + "0", this.#cardNumberPrototype);
+ this.addOrUpdateLine(card, lineId + "1", this.#expirationPrototype);
+ this.addOrUpdateLine(card, lineId + "2", this.#holderNamePrototype);
+ });
+ this.afterReloadingDataSource();
+ }
+
+ #reloadEmptyDataSource() {
+ this.lines.length = 0;
+ //todo: user can enable credit cards by activating header line
+ this.#header.value = this.#cardsDisabledMessage;
+ this.refreshAllLinesOnScreen();
+ }
+
+ observe(_subj, topic, message) {
+ if (
+ topic == "formautofill-storage-changed" ||
+ message == "extensions.formautofill.creditCards.enabled"
+ ) {
+ this.#reloadDataSource();
+ }
+ }
+}