diff --git a/src/app/loaders/Accessibility.js b/src/app/loaders/Accessibility.js index 9436ed2..f1bb32a 100644 --- a/src/app/loaders/Accessibility.js +++ b/src/app/loaders/Accessibility.js @@ -2,18 +2,23 @@ import { CoreLoader, CoreLoaderSkip, CoreLoaderResult } from "@Core/Init/CoreLoa import Listeners from "@Core/Services/Listeners" import SettingsStorage from "@Core/Services/Settings/SettingsStorage" import DOMController from "@DOMPath/DOM/Helpers/domController" +import { Scaffold } from "@Environment/Library/DOM/buildBlock" CoreLoader.registerTask({ id: "accesibility", presence: "Accesibility features", async task() { - if (!await SettingsStorage.getFlag("enable_tab_navigation")) return new CoreLoaderSkip() + if (!await SettingsStorage.getFlag("enable_tab_navigation")) { + return new CoreLoaderSkip() + } DOMController.setConfig({ eventsOnClickAutoTabIndex: true, }) - Listeners.add(document, "keypress", (a) => { if (a.code === "Enter") { document.activeElement.click() } }) + Listeners.add(document, "keypress", (a) => { if (a.keyCode === 13 || a.which === 13) { document.activeElement.click() } }) + + Scaffold.accessibility = true return new CoreLoaderResult() }, diff --git a/src/app/loaders/CrossMessenger.js b/src/app/loaders/CrossMessenger.js index d413b16..0b0aa7a 100644 --- a/src/app/loaders/CrossMessenger.js +++ b/src/app/loaders/CrossMessenger.js @@ -4,9 +4,8 @@ import Listeners from "@Core/Services/Listeners" import Auth from "@App/modules/mono/services/Auth" import SettingsStorage from "@Core/Services/Settings/SettingsStorage" import { CoreLoader, CoreLoaderSkip, CoreLoaderResult } from "@Core/Init/CoreLoader" +import resetApp from "@App/tools/interaction/resetApp" import { Report } from "@Core/Services/Report" -import DBTool from "@Core/Tools/db/DBTool" -import ObjectStoreTool from "@Core/Tools/db/ObjectStoreTool" const trustedOrigins = __TRUSTED_ORIGINS @@ -19,20 +18,7 @@ async function messageListener(m) { try { if (m.data.command === "clear") { - let dbs = [{ name: "AuthStorage" }, { name: "OfflineCache" }, { name: "SettingsStorage" }, { name: "StatementStorage" }] - if ("databases" in indexedDB) { - dbs = await window.indexedDB.databases() - } - await Promise.all(dbs.map(async (db) => new Promise(async (resolve, reject) => { - try { - const dbase = new DBTool(db.name, null) - const osList = await dbase.getTablesList() - await Promise.all(osList.map((r) => new ObjectStoreTool(dbase, r).clear())) - resolve() - } catch (e) { - reject(e) - } - }))) + await resetApp() } else if (m.data.command === "import-accounts") { const accounts = (await Auth.accountsDB()) await Promise.all(m.data.accounts.map((item) => accounts.put(item))) diff --git a/src/app/loaders/Presets/AutoErrorReport.js b/src/app/loaders/Presets/AutoErrorReport.js new file mode 100644 index 0000000..a787d81 --- /dev/null +++ b/src/app/loaders/Presets/AutoErrorReport.js @@ -0,0 +1,49 @@ +import { Report, ReportLogger, ReportStorage } from "@Core/Services/Report" +import errorToObject from "@Core/Tools/transformation/object/errorToObject" +import PWA from "@App/modules/main/PWA" +import DBUserPresence from "@Core/Services/DBUserPresence" + +const beaconsAllowed = ( + PWA.isWG + || localStorage.getItem("beacon_error_reports") !== "0" +) + +if (beaconsAllowed) { + Report.newHook((report) => { + if (report.level >= 3) { + navigator.sendBeacon("https://sominemo.com/mono/help/report/beacon", + JSON.stringify( + { + log: errorToObject(report.log), + v: `${PWA.version}/${PWA.branch}/${PWA.buildDate}`, + }, + )) + } + }, "errorReporting") +} + +const autosendAllowed = ( + PWA.isWG + || localStorage.getItem("reports_auto_sending") === "1" +) + +if (autosendAllowed) { + Report.newHook((report) => { + if (report.level >= 3) { + DBUserPresence.get("ReportData").functions.find((e) => e.name === "send").handler(report.log) + .then(() => { + Report.add("Report sent", ["report.storage"]) + }) + } + }, "reportAutoSend") +} + +const extendedLoggingAllowed = ( + PWA.buildFlag("local") + || localStorage.getItem("reports_extended_logging") === "1" +) + +if (extendedLoggingAllowed) { + ReportLogger.loggingLevel = -2 + ReportStorage.loggingLevel = -2 +} diff --git a/src/app/loaders/Presets/FatalErrorListener.js b/src/app/loaders/Presets/FatalErrorListener.js new file mode 100644 index 0000000..fc64984 --- /dev/null +++ b/src/app/loaders/Presets/FatalErrorListener.js @@ -0,0 +1,379 @@ +/* globals __PACKAGE_FEEDBACK */ +import CriticalLoadErrorListener from "@Core/Services/CriticalLoadErrorListener" +import SplashScreenController from "@Environment/Loaders/SplashScreenController" +import { $ } from "@Core/Services/Language/handler" +import App from "@Core/Services/app" +import { ReportStorage } from "@Core/Services/Report" +import Axios from "axios" +import download from "@App/tools/interaction/download" +import resetApp from "@App/tools/interaction/resetApp" + +function l(name, fallback) { + try { + return $(name, {}, false) + } catch (e) { + return fallback + } +} + +let FAInited = false + +CriticalLoadErrorListener.listener = async (e, consoleIt = true) => { + function escapeHTML(unsafeText) { + const div = document.createElement("div") + div.innerText = unsafeText + return div.innerHTML + } + + if (consoleIt) console.error(e) + + + if (!document.body) await new Promise((resolve) => { document.onload = resolve }) + try { + SplashScreenController.enabled = false + SplashScreenController.splashElement.remove() + } catch (er) { + // Handle error + } + + let error + if (typeof e === "object") { + const filename = e.fileName || e.filename || "[unknown file]" + const lineno = e.lineNumber || e.lineno || "?" + const colno = e.columnNumber || e.colno || "??" + const stack = e.stack || false + + error = (e.fileName || e.filename + || e.lineNumber || e.lineno + || e.columnNumber || e.colno + ? `${e.message} on ${filename}:${lineno}:${colno} ${stack}` : stack) + } else error = String(e) + const ua = window.navigator.userAgent + let text = `${escapeHTML(error)}\n\n${escapeHTML(ua)}` + + if (FAInited) { + FAInited(text) + return + } + + FAInited = (data) => { + document.getElementById("em-error").innerHTML += `\n\n-------------------------\n\n${data}` + text += `\n\n-------------------------\n\n${data}` + } + + const icons = { + sad: require("@Resources/images/vector/css/fatal_error.svg").default, + clear: require("@Resources/images/vector/css/clear.svg").default, + export: require("@Resources/images/vector/css/export.svg").default, + help: require("@Resources/images/vector/css/help.svg").default, + replay: require("@Resources/images/vector/css/replay.svg").default, + reset: require("@Resources/images/vector/css/reset.svg").default, + send: require("@Resources/images/vector/css/send.svg").default, + more: require("@Resources/images/vector/css/more.svg").default, + } + + const strings = { + error: l("fatal_error", "Сталась помилка"), + explainer: l("fatal_error/explainer", "Застосунок не може завантажитись"), + actions: l("fatal_error/actions", "Що ви можете зробити:"), + send: { + title: l("fatal_error/actions/send/title", "Надіслати звіт"), + info: l("fatal_error/actions/send/info", "Для аналізу та виправлення"), + title_sent: l("fatal_error/actions/send/title_sent", "Звіт надіслано"), + info_sent: l("fatal_error/actions/send/info_sent", "Дякуємо за допомогу"), + info_auto: l("fatal_error/actions/send/info_sent", "Включене автонадсилання"), + }, + more: { + title: l("fatal_error/actions/more/title", "Інші варіанти"), + info: l("fatal_error/actions/more/info", "Звітування, кеш, скидання налаштувань"), + }, + replay: { + title: l("fatal_error/actions/replay/title", "Відтворити ще раз"), + info: l("fatal_error/actions/replay/info", "З записом детального звіту"), + info_enabled: l("fatal_error/actions/replay/info_enabled", "Детальний звіт вже ведеться"), + }, + clear: { + title: l("fatal_error/actions/clear/title", "Очистити кеш"), + info: l("fatal_error/actions/clear/info", "Застосунок не працюватиме без мережі"), + }, + reset: { + title: l("fatal_error/actions/reset/title", "Виконати скидання"), + info: l("fatal_error/actions/reset/info", "Акаунти та налаштування буде втрачено"), + }, + help: { + title: l("fatal_error/actions/help/title", "Звернутись по допомогу"), + info: l("fatal_error/actions/help/info", "Зворотній зв'язок у чаті Telegram"), + }, + export: { + title: l("fatal_error/actions/export/title", "Експортувати звіт"), + info: l("fatal_error/actions/export/info", "Буде згенеровано файл"), + }, + } + + function card(name, info, icon, handler, invert = false, opacity = 1) { + const c = document.createElement("div") + c.classList.add("em-button-card") + if (invert) c.classList.add("invert") + c.addEventListener("click", handler) + if (opacity !== 1) c.style.opacity = opacity + if (opacity === 1) c.tabIndex = 1 + + const i = document.createElement("div") + i.classList.add("em-button-card-icon") + + const img = document.createElement("img") + img.src = icon + i.append(img) + c.append(i) + + const b = document.createElement("div") + b.classList.add("em-button-card-body") + + const n = document.createElement("div") + n.innerHTML = name + + const inf = document.createElement("div") + inf.innerHTML = info + + b.append(n) + b.append(inf) + c.append(b) + + return c + } + + document.body.innerHTML = ` + + +
+
${strings.error}
+
${strings.explainer}
+
${text}
+ + ${strings.actions} +
+
+
` + const pre = document.getElementById("em-error") + pre.onclick = function preClick() { + this.classList.add("open") + } + document.addEventListener("keypress", (a) => { if (a.code === "Enter") { document.activeElement.click() } }) + + const cont = document.getElementById("em-actions") + cont.append( + card(strings.send.title, strings.send.info, icons.send, async () => { + const db = JSON.stringify( + await ReportStorage.export(), + ) + + const log = { + error: text, + report: db, + v: `${App.version}/${App.branch}/${App.buildDate}`, + } + + await Axios({ + method: "post", + url: "https://sominemo.com/mono/help/report/beacon", + data: log, + }) + }, true), + card(strings.help.title, strings.help.info, icons.help, () => { + window.open(__PACKAGE_FEEDBACK, "_blank") + }), + card(strings.more.title, strings.more.info, icons.more, function showMore() { + this.remove() + cont.append( + card(strings.clear.title, strings.clear.info, icons.clear, async () => { + if ("caches" in window) { + (await window.caches.keys()).forEach(async (name) => { + await window.caches.delete(name) + }) + } + + const registrations = await navigator.serviceWorker.getRegistrations() + await Promise.all(registrations + .map((registration) => registration.unregister())) + + window.location.reload() + }), + card(strings.reset.title, strings.reset.info, icons.reset, () => { + resetApp() + window.location.reload() + }), + card(strings.replay.title, strings.replay.info, icons.replay, () => { + localStorage.setItem("reports_extended_logging", "1") + window.location.reload() + }), + card(strings.export.title, strings.export.info, icons.export, async () => { + const db = JSON.stringify(await ReportStorage.export()) + + download([db], "text/plain", "app-log.json") + }), + ) + }), + ) +} diff --git a/src/app/loaders/Presets/index.js b/src/app/loaders/Presets/index.js index da680e6..16110e8 100644 --- a/src/app/loaders/Presets/index.js +++ b/src/app/loaders/Presets/index.js @@ -1,4 +1,6 @@ import "./Polyfills" +import "./AutoErrorReport" +import "./FatalErrorListener" import "./CoreLoaderListener" import "./LanguageSettings" import "./ThemeSettings" diff --git a/src/app/loaders/SettingsLayout/Privacy.js b/src/app/loaders/SettingsLayout/Privacy.js new file mode 100644 index 0000000..a686817 --- /dev/null +++ b/src/app/loaders/SettingsLayout/Privacy.js @@ -0,0 +1,94 @@ +/* global __PACKAGE_ANALYTICS */ +import { SettingsSectionElement, SettingsGroupContainer } from "@App/modules/main/settings" +import { $$ } from "@Core/Services/Language/handler" +import FlagsUI from "@App/modules/main/flags" +import PWA from "@App/modules/main/PWA" +import DOM from "@DOMPath/DOM/Classes/dom" + +export default function generatePrivacyLayout(act) { + const sectionNameAnalytics = "privacy-analytics" + const groupNameAnalytics = "privacy-analytics-group" + + const sectionAnalytics = act.createSection({ + id: sectionNameAnalytics, + options: { + name: $$("settings/privacy/analytics"), + }, + dom: SettingsSectionElement, + display: () => true || __PACKAGE_ANALYTICS, + }).getSection(sectionNameAnalytics) + + const analyticsGroup = sectionAnalytics.createGroup({ + id: groupNameAnalytics, + options: {}, + dom: SettingsGroupContainer, + }).getGroup(groupNameAnalytics) + + const sectionNameReports = "privacy-reports" + const groupNameReports = "privacy-reports-group" + + const sectionReports = act.createSection({ + id: sectionNameReports, + options: { + name: $$("settings/privacy/reports"), + }, + dom: SettingsSectionElement, + }).getSection(sectionNameReports) + + const reportsGroup = sectionReports.createGroup({ + id: groupNameReports, + options: {}, + dom: () => new DOM({ new: "div" }), + }).getGroup(groupNameReports) + + analyticsGroup.createItem({ + id: "privacy-analytics-control", + options: {}, + dom() { + return FlagsUI.renderSwitch(null, null, "deny_analytics", { showID: false, locked: PWA.isWG }) + }, + }) + + reportsGroup.createItem({ + id: "privacy-reports-send-errors", + options: {}, + dom() { + return FlagsUI.renderSwitch( + $$("settings/privacy/send_errors"), + $$("settings/privacy/send_errors/info"), + "beacon_error_reports", + { + ls: true, showID: false, lsDefault: 1, locked: PWA.isWG, + }, + ) + }, + }) + reportsGroup.createItem({ + id: "privacy-reports-send-reports", + options: {}, + dom() { + return FlagsUI.renderSwitch( + $$("settings/privacy/send_reports"), + $$("settings/privacy/send_reports/info"), + "reports_auto_sending", + { + ls: true, showID: false, lsDefault: PWA.debug, locked: PWA.isWG, + }, + ) + }, + }) + reportsGroup.createItem({ + id: "privacy-reports-extended-logging", + options: {}, + dom() { + return FlagsUI.renderSwitch( + $$("settings/privacy/debug_log"), + $$("settings/privacy/debug_log/info"), + "reports_extended_logging", + { + ls: true, showID: false, lsDefault: PWA.buildFlag("local"), + }, + ) + }, + }) +} diff --git a/src/app/loaders/UI/DBControllers.js b/src/app/loaders/UI/DBControllers.js index 5d8534f..4bb5169 100644 --- a/src/app/loaders/UI/DBControllers.js +++ b/src/app/loaders/UI/DBControllers.js @@ -1,6 +1,6 @@ import DBUserPresence from "@Core/Services/DBUserPresence" import { $$ } from "@Core/Services/Language/handler" -import { ReportStorage } from "@Core/Services/Report" +import { ReportStorage, Report } from "@Core/Services/Report" import download from "@App/tools/interaction/download" import { CoreLoader } from "@Core/Init/CoreLoader" import OfflineCache from "@App/modules/mono/services/OfflineCache" @@ -9,6 +9,10 @@ import Auth from "@App/modules/mono/services/Auth" import Prompt from "@Environment/Library/DOM/elements/prompt" import SettingsStorage from "@Core/Services/Settings/SettingsStorage" import size from "@Core/Tools/objects/size" +import PWA from "@App/modules/main/PWA" +import delayAction from "@Core/Tools/objects/delayAction" +import Axios from "axios" +import errorToObject from "@Core/Tools/transformation/object/errorToObject" CoreLoader.registerTask({ id: "db-presence", @@ -55,6 +59,37 @@ CoreLoader.registerTask({ } }), }, + { + name: "send", + handler(error = null) { + return new Promise((resolve, reject) => { + delayAction(async () => { + try { + const db = JSON.stringify( + await ReportStorage.export({ currentOnly: true }), + ) + + const log = { + error: (error === null ? `Manual report ${Report.session.id}` : errorToObject(error)), + report: db, + v: `${PWA.version}/${PWA.branch}/${PWA.buildDate}`, + } + + resolve( + await Axios({ + method: "post", + url: "https://sominemo.com/mono/help/report/beacon", + data: log, + }), + ) + } catch (e) { + Report.add(e, ["report.storage.error"]) + reject(e) + } + }) + }) + }, + }, { name: "export", async handler() { diff --git a/src/app/loaders/UI/Nav.js b/src/app/loaders/UI/Nav.js index 935d365..d687318 100644 --- a/src/app/loaders/UI/Nav.js +++ b/src/app/loaders/UI/Nav.js @@ -1,4 +1,4 @@ -import { Nav } from "@Environment/Library/DOM/buildBlock" +import { Nav, Scaffold } from "@Environment/Library/DOM/buildBlock" import Navigation from "@Core/Services/navigation" import { $$ } from "@Core/Services/Language/handler" @@ -10,12 +10,7 @@ Nav.constantNavMenu = [ Navigation.url = { module: "settings" } }, }, - { - icon: "info", - title: $$("about/app"), - handler() { - Navigation.url = { module: "about" } - }, - }, - ] + +Scaffold.enableAccessibilitySign = $$("enable_accessibility") +Scaffold.skipNavSign = $$("skip_nav") diff --git a/src/app/loaders/UI/SettingsLayoutLoader.js b/src/app/loaders/UI/SettingsLayoutLoader.js index 2613dac..a61b852 100644 --- a/src/app/loaders/UI/SettingsLayoutLoader.js +++ b/src/app/loaders/UI/SettingsLayoutLoader.js @@ -24,6 +24,7 @@ import generateDBSettingsLayout from "../SettingsLayout/DBPresence" import generateLanguageList from "../SettingsLayout/LanguageList" import generateTFSettingsLayout from "../SettingsLayout/Transformators" import generateNotificationsSettingsLayout from "../SettingsLayout/Notifications" +import generatePrivacyLayout from "../SettingsLayout/Privacy" function sideLogo(icon, text, moreText) { return new Align([ @@ -66,6 +67,11 @@ CoreLoader.registerTask({ dom: SettingsActContainer, options: { name: $$("settings/notifications") }, }) + .createAct({ + id: "privacy", + dom: SettingsActContainer, + options: { name: $$("settings/privacy") }, + }) const isPushSupported = "ServiceWorkerRegistration" in window @@ -84,11 +90,6 @@ CoreLoader.registerTask({ .getSection("general") .createGroup({ id: "main-group", dom: SettingsGroupContainer, options: {} }) .getGroup("main-group") - .createItem({ - dom: SettingsActLink, options: [() => { Navigation.url = { module: "about" } }, sideLogo("info", $$("about/app"), $("settings/descriptions/about_app"))], id: "about-screen-link", - }) - .createItem({ dom: SettingsActLink, options: ["storage", sideLogo("storage", $$("settings/storage"), $("settings/descriptions/storage"))], id: "storage-link" }) - .createItem({ dom: SettingsActLink, options: ["language", sideLogo("translate", $$("settings/language"), $("settings/descriptions/language"))], id: "language-link" }) if ("serviceWorker" in navigator) { settingsMain.createItem({ @@ -112,6 +113,14 @@ CoreLoader.registerTask({ }) } + settingsMain + .createItem({ dom: SettingsActLink, options: ["storage", sideLogo("storage", $$("settings/storage"), $("settings/descriptions/storage"))], id: "storage-link" }) + .createItem({ dom: SettingsActLink, options: ["language", sideLogo("translate", $$("settings/language"), $("settings/descriptions/language"))], id: "language-link" }) + .createItem({ dom: SettingsActLink, options: ["privacy", sideLogo("https", $$("settings/privacy"), $("settings/privacy/info"))], id: "privacy-link" }) + .createItem({ + dom: SettingsActLink, options: [() => { Navigation.url = { module: "about" } }, sideLogo("info", $$("about/app"), $("settings/descriptions/about_app"))], id: "about-screen-link", + }) + layout.getAct("settings").getSection("auth-promo") .createGroup({ id: "auth-promo-group", dom: SettingsGroupContainer, options: {}, @@ -273,12 +282,10 @@ CoreLoader.registerTask({ .createItem({ dom: SettingsActLink, options: [() => { Navigation.url = { module: "flags" } }, sideLogo("category", $$("experiments"), $("settings/descriptions/experiments"))], id: "experiments-menu-link" }) generateDBSettingsLayout(layout.getAct("storage")) - generateLanguageList(layout.getAct("language")) - generateTFSettingsLayout(layout.getAct("transformators")) - generateNotificationsSettingsLayout(layout.getAct("notifications")) + generatePrivacyLayout(layout.getAct("privacy")) SettingsLayoutManager.applyLayout(layout) }, diff --git a/src/app/loaders/UI/WGWarning.js b/src/app/loaders/UI/WGWarning.js index 4c6b1a0..e645c61 100644 --- a/src/app/loaders/UI/WGWarning.js +++ b/src/app/loaders/UI/WGWarning.js @@ -1,3 +1,4 @@ +/* global __PACKAGE_FEEDBACK */ import Toast from "@Environment/Library/DOM/elements/toast" import Prompt from "@Environment/Library/DOM/elements/prompt" import { CoreLoader, CoreLoaderSkip, CoreLoaderResult } from "@Core/Init/CoreLoader" @@ -27,7 +28,7 @@ CoreLoader.registerTask({ { content: "Telegram", handler() { - window.open("tg://join?invite=BEBMsBLX6NclKYzGkNlGNw", "_blank") + window.open(__PACKAGE_FEEDBACK, "_blank") SettingsStorage.setFlag("wg_warning_seen", true) w.close() }, diff --git a/src/app/modules/main/PWA.js b/src/app/modules/main/PWA.js index 04675e8..1346fe6 100644 --- a/src/app/modules/main/PWA.js +++ b/src/app/modules/main/PWA.js @@ -1,4 +1,4 @@ -/* global __PACKAGE_WG, __PACKAGE_ANALYTICS */ +/* global __PACKAGE_ANALYTICS */ import App from "@Core/Services/app" import { SVG } from "@Environment/Library/DOM/basic" @@ -13,12 +13,11 @@ import getCounter from "@Core/Tools/objects/counter" import AlignedContent from "@Environment/Library/DOM/object/AlignedContent" import { CoreLoader } from "@Core/Init/CoreLoader" import Prompt from "@Environment/Library/DOM/elements/prompt" -import FlagsUI from "./flags" import { SettingsActLink } from "./settings" export default class PWA extends App { static get isWG() { - return __PACKAGE_WG + return this.buildFlag("wg") } static get analyticsAllowed() { @@ -80,18 +79,11 @@ export default class PWA extends App { [ { content: new TwoSidesWrapper($$("about/build_date"), this.buildDate) }, { content: new TwoSidesWrapper($$("about/branch"), this.branch) }, - ...(this.debug ? [{ content: new TwoSidesWrapper($$("about/debug"), this.debug.toString()) }] : []), - ...(this.isWG ? [{ content: new TwoSidesWrapper("Work Group build", "true") }] : []), + { content: new TwoSidesWrapper($$("about/build_flags"), this.buildFlags.join(", ")) }, ...("Windows" in window ? [{ content: new TwoSidesWrapper("WinRT", "true") }] : []), ], {}, true, ))) - - // if (__PACKAGE_ANALYTICS) { - if (!PWA.isWG) { - w.render(FlagsUI.renderSwitch(null, null, "deny_analytics")) - } - // } } static disclaimer() { diff --git a/src/app/modules/main/flags.js b/src/app/modules/main/flags.js index b493893..2ad9cc0 100644 --- a/src/app/modules/main/flags.js +++ b/src/app/modules/main/flags.js @@ -116,25 +116,36 @@ export default class FlagsUI { })) } - static renderSwitch(title, about, id) { + static renderSwitch(title, about, id, { + ls = false, showID = true, lsDefault = false, locked = false, + } = {}) { const sw = new SwitchLabel( [0, (n) => { - SettingsStorage.setFlag(id, n) + if (!ls) { + SettingsStorage.setFlag(id, n) + } else { + localStorage.setItem(id, n) + } }, { locked: true }], new DOM({ new: "div", content: [ new DOM({ new: "div", content: title || $(`experiments/about/${id}/title`), style: { fontWeight: "500", fontSize: "20px" } }), - new DOM({ new: "div", content: id, style: { color: "lightgray", fontSize: "12px" } }), + ...(showID ? [new DOM({ new: "div", content: id, style: { color: "lightgray", fontSize: "12px" } })] : []), ], id: `flag-id-${id}`, }), ) delayAction(async () => { - const r = await SettingsStorage.getFlag(id) + let r = (!ls ? await SettingsStorage.getFlag(id) + : localStorage.getItem(id)) + if (ls) { + if (r === null || r === undefined) r = lsDefault + else r = Number.parseInt(r, 10) + } sw.switch.changeState(!!r) - sw.switch.changeLock(false) + if (!locked) sw.switch.changeLock(false) }) return new Card([ sw, diff --git a/src/app/res/images/vector/css/clear.svg b/src/app/res/images/vector/css/clear.svg new file mode 100644 index 0000000..17ac045 --- /dev/null +++ b/src/app/res/images/vector/css/clear.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/app/res/images/vector/css/export.svg b/src/app/res/images/vector/css/export.svg new file mode 100644 index 0000000..770515f --- /dev/null +++ b/src/app/res/images/vector/css/export.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/app/res/images/vector/css/fatal_error.svg b/src/app/res/images/vector/css/fatal_error.svg new file mode 100644 index 0000000..361a0fc --- /dev/null +++ b/src/app/res/images/vector/css/fatal_error.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/app/res/images/vector/css/help.svg b/src/app/res/images/vector/css/help.svg new file mode 100644 index 0000000..052b3f3 --- /dev/null +++ b/src/app/res/images/vector/css/help.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/app/res/images/vector/css/more.svg b/src/app/res/images/vector/css/more.svg new file mode 100644 index 0000000..1a9daf5 --- /dev/null +++ b/src/app/res/images/vector/css/more.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/app/res/images/vector/css/replay.svg b/src/app/res/images/vector/css/replay.svg new file mode 100644 index 0000000..93bd269 --- /dev/null +++ b/src/app/res/images/vector/css/replay.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/app/res/images/vector/css/reset.svg b/src/app/res/images/vector/css/reset.svg new file mode 100644 index 0000000..c6f5cd2 --- /dev/null +++ b/src/app/res/images/vector/css/reset.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/app/res/images/vector/css/send.svg b/src/app/res/images/vector/css/send.svg new file mode 100644 index 0000000..0b9b8b5 --- /dev/null +++ b/src/app/res/images/vector/css/send.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/app/res/language/ru/strings.js b/src/app/res/language/ru/strings.js index 80c0ad1..ddfa254 100644 --- a/src/app/res/language/ru/strings.js +++ b/src/app/res/language/ru/strings.js @@ -259,6 +259,24 @@ export default { no_push_services_hint_body: "Узнайте, как работают уведомления", no_push_services_hint_link: "https://sominemo.com/mono/help/article/ru/how-push-servers-work", }, + privacy: { + "": "приватность", + info: "Отправка отчётов об ошибках и аналитики", + analytics: "аналитика", + reports: "отчёты", + send_errors: { + "": "делиться данными об ошибках", + info: "Отправка минимальной информации для устранения ошибки. Включает в себя текст ошибки, место её возникновения, версию приложения, название и версию браузера/ОС", + }, + send_reports: { + "": "автоматически отправлять отчёты", + info: "Отправка журнала событий приложения. Включает в себя события и ошибки, статистику использования, версию приложения, информацию о браузере и ОС", + }, + debug_log: { + "": "вести подробный отчёт", + info: "Сбор всей возможной информации для эффективной отладки. Не включайте, если вам это не нужно", + }, + }, actions: { open_about: "о приложении", go_main: "на главную", @@ -331,6 +349,7 @@ export default { build_date: "дата сборки", branch: "ветка", debug: "отладка", + build_flags: "флаги сборки", disclaimer_title: "дисклеймер", disclaimer: `Mono PWA не является официальным приложением и не относится каким-либо образом к monobank. Приложение использует общедоступное API и не передаёт данные о использовании на хранение кому-либо. @@ -342,7 +361,7 @@ export default { наиболее частая категория транзакции и т.п., через сервис Google Analytics. Ваши имя, фамилия, уникальные идентификаторы и другие чувствительные данные не подлежат передаче кому-либо. Все передаваемые данные используются разработчиком исключительно с целью анализа аудитории, улучшая таким образом опыт использования программы. Вы всегда можете отказаться от сбора данных в -меню "Эксперименты" (/flags) или включив функцию "Do Not Track" в настройках браузера. +меню "Приватность" или включив функцию "Do Not Track" в настройках браузера. Вы всегда в праве обратиться за исходным кодом приложения по адресу me@sominemo.com, изменять, публиковать, исполнять код в любой форме, при этом отвественность за содержимое уже будет лежать именно на вас.`, }, @@ -403,6 +422,45 @@ export default { not_supported_title: "Функция недоступна", not_supported_text: "Уведомления не поддерживаются в браузере Safari на MacOS, а также в любом браузере на iOS/iPadOS и движке WebKit в целом. Это связано с тем, что Apple не следует Web-стандартам и ограничивает разработчиков. Для того, чтобы иметь возможность отправлять уведомления пользователям Safari, Apple требует наличие аккаунта разработчика, что означает наличие MacBook и регулярной платы $100/год. Mono PWA является бесплатным приложением, разрабатываемым на безвозмездной основе, разработчик которого такими ресурсами не обладает. \n\nРассмотрите вариант использования другого браузера, если вы на MacOS. \nРассмотрите вариант использования другого устройства, если вы на iOS. \n\nСлава Apple.", }, + fatal_error: { + "": "Возникла ошибка", + explainer: "Приложение не может запуститься", + actions: { + "": "Что вы можете сделать:", + send: { + title: "Отправить отчёт", + info: "Для анализа и исправления", + title_sent: "Отчёт отправлен", + info_sent: "Благодарим за помощь", + info_auto: "Включена автоотправка", + }, + more: { + title: "Другие варианты", + info: "Отчёты, кэш, сброс настроек", + }, + replay: { + title: "Воспроизвести ошибку", + info: "С записью подробного отчёта", + info_enabled: "Подробный отчёт уже записывается", + }, + clear: { + title: "Очистить кэш", + info: "Приложение не запустится без сети", + }, + reset: { + title: "Выполнить сброс", + info: "Аккаунты и настройки будут утеряны", + }, + help: { + title: "Обратиться за помощью", + info: "Обратная связь в чате Telegram", + }, + export: { + title: "Экспортировать отчёт", + info: "Будет сгенерирован файл", + }, + }, + }, unexpected_error: "ой!", select_option: "выберите из списка", tap_to_change: "нажмите, чтобы изменить", @@ -424,4 +482,6 @@ export default { tip: "совет", hint: "подсказка", reload: "перегрузить", + enable_accessibility: "включить специальные возможности", + skip_nav: "пропуск меню", } diff --git a/src/app/res/language/uk/strings.js b/src/app/res/language/uk/strings.js index 3849d59..b0ab26e 100644 --- a/src/app/res/language/uk/strings.js +++ b/src/app/res/language/uk/strings.js @@ -260,6 +260,24 @@ export default { no_push_services_hint_body: "Дізнайтесь, як працюють сповіщення", no_push_services_hint_link: "https://sominemo.com/mono/help/article/uk/how-push-servers-work", }, + privacy: { + "": "приватність", + info: "Надсилання звітів про помилки та аналітики", + analytics: "аналітика", + reports: "звіти", + send_errors: { + "": "надавати дані про помилки", + info: "Надсилання мінімальної інформації для виправлення. Містить у собі текст помилки, місце, де вона трапилась, версію застосунку, назву і версію браузера/ОС", + }, + send_reports: { + "": "автоматично надсилати звіти", + info: "Надсилання журналу подій застосунка. Містить у собі події та помилки, статистику використання, версію застосунку, назву і версію браузера та ОС", + }, + debug_log: { + "": "вести докладний звіт", + info: "Збір усієї можливої інформації для зневадження. Не вмикайте, якщо вам це не потрібно", + }, + }, actions: { open_about: "про програму", go_main: "на головну", @@ -333,6 +351,7 @@ export default { build_date: "дата збирання", branch: "гілка", debug: "зневаджування", + build_flags: "прапори збирання", disclaimer_title: "дисклеймер", disclaimer: `Mono PWA не є офіційним додатком і не стосується monobank будь-яким чином. Додаток використовує загальнодоступне API і не передає дані про використання на зберігання будь-кому. @@ -344,7 +363,7 @@ export default { найчастіша категорія транзакції, тощо, через сервіс Google Analytics. Ваші ім'я, прізвище, унікальні ідентифікатори та інші чутливі дані не підлягають передачі будь-кому. Усі дані, що передаються, використовуються розробником виключно з метою аналізу аудиторії, покращуючи у такий спосіб досвід використання програми. Ви завжди можете відмовитись від збирання -даних у меню "Експерименти" (/flags) або включивши функцію "Do Not Track" у налаштуваннях браузера. +даних у меню "Приватність" або включивши функцію "Do Not Track" у налаштуваннях браузера. Ви завжди в праві звернутися за сирцевим кодом програми за адресою me@sominemo.com, змінювати, публікувати, виконувати код в будь-якій формі, при цьому відповідальність за вміст вже буде саме на вас.`, }, @@ -404,6 +423,45 @@ export default { not_supported_title: "Функція недоступна", not_supported_text: "Сповіщення не підтримуються в браузері Safari на MacOS, а також у будь-якому браузері на iOS/iPadOS та движку WebKit у цілому. Це пов'язано з тим, що Apple не дотримується Web-стандартів та обмежує розробників. Для того, щоб мати можливість надсилати сповіщення користувачам Safari, Apple вимагає наявність аккаунта розробника, що означає наявність MacBook і регулярну плату $100/рік. Mono PWA є безкоштовною програмою, що розробляється на безоплатній основі, розробник якого такими ресурсами не володіє. \n\nРозгляньте варіант використання іншого браузера, якщо ви на MacOS. \nРозгляньте варіант використання іншого пристрою, якщо ви на iOS. \n\nСлава Apple.", }, + fatal_error: { + "": "Сталась помилка", + explainer: "Застосунок не може завантажитись", + actions: { + "": "Що ви можете зробити:", + send: { + title: "Надіслати звіт", + info: "Для аналізу та виправлення", + title_sent: "Звіт надіслано", + info_sent: "Дякуємо за допомогу", + info_auto: "Включене автонадсилання", + }, + more: { + title: "Інші варіанти", + info: "Звітування, кеш, скидання налаштувань", + }, + replay: { + title: "Відтворити помилку", + info: "З записом детального звіту", + info_enabled: "Детальний звіт вже ведеться", + }, + clear: { + title: "Очистити кеш", + info: "Застосунок не працюватиме без мережі", + }, + reset: { + title: "Виконати скидання", + info: "Акаунти та налаштування буде втрачено", + }, + help: { + title: "Звернутись по допомогу", + info: "Зворотній зв'язок у чаті Telegram", + }, + export: { + title: "Експортувати звіт", + info: "Буде згенеровано файл", + }, + }, + }, unexpected_error: "ой!", select_option: "оберіть зі списку", tap_to_change: "натисніть, щоб змінити", @@ -425,4 +483,6 @@ export default { tip: "порада", hint: "підказка", reload: "перезавантажити", + enable_accessibility: "включити спеціальні можливості", + skip_nav: "минути меню", } diff --git a/src/app/tools/interaction/resetApp.js b/src/app/tools/interaction/resetApp.js new file mode 100644 index 0000000..301637c --- /dev/null +++ b/src/app/tools/interaction/resetApp.js @@ -0,0 +1,22 @@ +export default async function resetApp() { + localStorage.clear() + + let dbs = [ + { name: "AuthStorage" }, { name: "OfflineCache" }, + { name: "SettingsStorage" }, { name: "StatementStorage" }, + { name: "ReportStorage" }, { name: "userActivityHistory" }, + ] + + if ("databases" in indexedDB) { + dbs = await window.indexedDB.databases() + } + await Promise.all(dbs.map(async (db) => new Promise(async (resolve, reject) => { + try { + const r = indexedDB.deleteDatabase(db.name) + r.onsuccess = resolve + r.onerror = reject + } catch (e) { + reject(e) + } + }))) +} diff --git a/src/core b/src/core index 17fbf8d..9fd238f 160000 --- a/src/core +++ b/src/core @@ -1 +1 @@ -Subproject commit 17fbf8d75f8579762795ff4b33b0f819a027a96e +Subproject commit 9fd238fd2343550f10d5084491dbe7994be3a7d2 diff --git a/src/environment b/src/environment index 0dc067f..9d48021 160000 --- a/src/environment +++ b/src/environment @@ -1 +1 @@ -Subproject commit 0dc067f69bac6bb31209a5349adb2b740f92202a +Subproject commit 9d4802132684916d56e5d006560147b561516849 diff --git a/webpack/webpack.config.js b/webpack/webpack.config.js index a804cca..eb1ee65 100644 --- a/webpack/webpack.config.js +++ b/webpack/webpack.config.js @@ -52,13 +52,28 @@ const resolveAlias = { "@Themes": PATHS.themesGenerated, } +const buildFlags = [] + module.exports = (env = {}) => { const PROD = !!env.PRODUCTION const CHANGELOG = env.CHANGELOG || null PATHS.build = (env.LOCAL ? PATHS.localBuild : (env.WG ? PATHS.wgBuild : PATHS.build)) const ANALYTICS_TAG = (env.ANALYTICS ? (!env.WG ? "G-81RB2HPF8X" : "G-PEX3Q03WQ6") : null) - if (PROD) console.log("-- PRODUCTION BUILD --") + if (PROD) { + console.log("-- PRODUCTION BUILD --") + buildFlags.push("prod") + } else { + env.DEBUG = true + } + + if (env.CI) { + buildFlags.push("ci") + } + if (env.WG) buildFlags.push("wg") + if (env.DEBUG) buildFlags.push("debug") + if (env.LOCAL) buildFlags.push("local") + if (env.ANALYTICS) buildFlags.push("analytics") if (env.watch && !env.CI) { const cb = () => { @@ -75,15 +90,18 @@ module.exports = (env = {}) => { .on("unlink", cb) } + const feedbackLink = "tg://join?invite=BEBMsBLX6NclKYzGkNlGNw" + const mainDefine = { __PACKAGE_APP_NAME: JSON.stringify(builder.pack.description), __PACKAGE_VERSION_NUMBER: JSON.stringify(builder.pack.version), __PACKAGE_BRANCH: JSON.stringify((env.WG ? "workgroup" : builder.pack.config.branch)), __PACKAGE_BUILD_TIME: webpack.DefinePlugin.runtimeValue(() => JSON.stringify(fecha.format(new Date(), "DD.MM.YYYY HH:mm:ss")), true), + __PACKAGE_BUILD_FLAGS: JSON.stringify(buildFlags), __PACKAGE_CHANGELOG: JSON.stringify(CHANGELOG), - __PACKAGE_WG: JSON.stringify(!!env.WG), __PACKAGE_ANALYTICS: JSON.stringify(ANALYTICS_TAG), __PACKAGE_DOWNLOADABLE_LANG_PACKS: JSON.stringify(!!DOWNLOAD_LANG_PACKS), + __PACKAGE_FEEDBACK: JSON.stringify(feedbackLink), __MCC_CODES_EMOJI: JSON.stringify(mccEmojiMap), __TRUSTED_ORIGINS: JSON.stringify(trustedOrigins), } @@ -144,7 +162,7 @@ module.exports = (env = {}) => { entry: { index: path.join(PATHS.core, "Init", "index.js"), }, - ...(!PROD || env.DEBUG ? { devtool: "source-map" } : {}), + ...(env.DEBUG ? { devtool: "source-map" } : {}), output: { path: PATHS.build, chunkFilename: "[id].js",