diff --git a/biome.json b/biome.json index b991ec0f2..389b37960 100644 --- a/biome.json +++ b/biome.json @@ -1,29 +1,10 @@ { "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json", - "organizeImports": { - "enabled": true - }, + "formatter": { "enabled": true, "indentStyle": "tab" }, + "organizeImports": { "enabled": true }, "linter": { "enabled": true, - "rules": { - "recommended": false, - "complexity": { - "noStaticOnlyClass": "error", - "noUselessSwitchCase": "error", - "useFlatMap": "error" - }, - "style": { - "noNegationElse": "off", - "useForOf": "error", - "useNodejsImportProtocol": "error", - "useNumberNamespace": "error" - }, - "suspicious": { - "noDoubleEquals": "error", - "noThenProperty": "error", - "useIsArray": "error" - } - } + "rules": { "recommended": false } }, "files": { "include": ["src/**/*", "utils/**/*.js", "www/**/*.js", "www/res/**/*.css"], diff --git a/src/lib/acode.js b/src/lib/acode.js index 238a2c064..f2f77289d 100644 --- a/src/lib/acode.js +++ b/src/lib/acode.js @@ -1,3 +1,4 @@ +import ajax from "@deadlyjack/ajax"; import Contextmenu from "components/contextmenu"; import inputhints from "components/inputhints"; import Page from "components/page"; @@ -16,6 +17,7 @@ import prompt from "dialogs/prompt"; import select from "dialogs/select"; import fsOperation from "fileSystem"; import keyboardHandler from "handlers/keyboard"; +import purchaseListener from "handlers/purchase"; import windowResize from "handlers/windowResize"; import actionStack from "lib/actionStack"; import commands from "lib/commands"; @@ -154,9 +156,8 @@ export default class Acode { exec(key, val) { if (key in commands) { return commands[key](val); - } else { - return false; } + return false; } /** @@ -166,67 +167,117 @@ export default class Acode { * @returns {Promise} */ installPlugin(pluginId, installerPluginName) { - return new Promise(async (resolve, reject) => { - try { - const confirmation = await confirm( - strings["install"], - `Do you want to install plugin '${pluginId}'${installerPluginName ? ` requested by ${installerPluginName}` : ""}?`, - ); - - if (!confirmation) { - reject(new Error("User cancelled installation")); - return; - } - - const isPluginExists = await fsOperation( - Url.join(PLUGIN_DIR, pluginId), - ).exists(); - if (isPluginExists) { - reject(new Error("PLugin already installed")); - return; - } - - let purchaseToken = null; - - const pluginUrl = Url.join(constants.API_BASE, `plugin/${pluginId}`); - const remotePlugin = await fsOperation(pluginUrl) - .readFile("json") - .catch(() => { - reject(new Error("Failed to fetch plugin details")); - return null; - }); - - if (remotePlugin) { - if (Number.parseFloat(remotePlugin.price) > 0) { - try { - const [product] = await helpers.promisify(iap.getProducts, [ - remotePlugin.sku, - ]); - if (product) { - async function getPurchase(sku) { - const purchases = await helpers.promisify(iap.getPurchases); - const purchase = purchases.find((p) => - p.productIds.includes(sku), - ); - return purchase; - } - const purchase = await getPurchase(product.productId); - purchaseToken = purchase?.purchaseToken; - } - } catch (error) { - helpers.error(error); - reject(new Error("Failed to validate purchase")); - return; - } + return new Promise((resolve, reject) => { + confirm( + strings.install, + `Do you want to install plugin '${pluginId}'${installerPluginName ? ` requested by ${installerPluginName}` : ""}?`, + ) + .then((confirmation) => { + if (!confirmation) { + reject(new Error("User cancelled installation")); + return; } - } - const { default: installPlugin } = await import("lib/installPlugin"); - await installPlugin(pluginId, remotePlugin.name, purchaseToken); - resolve(); - } catch (error) { - reject(error); - } + fsOperation(Url.join(PLUGIN_DIR, pluginId)) + .exists() + .then((isPluginExists) => { + if (isPluginExists) { + reject(new Error("Plugin already installed")); + return; + } + + let purchaseToken; + let product; + const pluginUrl = Url.join( + constants.API_BASE, + `plugin/${pluginId}`, + ); + fsOperation(pluginUrl) + .readFile("json") + .catch(() => { + reject(new Error("Failed to fetch plugin details")); + return null; + }) + .then((remotePlugin) => { + if (remotePlugin) { + const isPaid = remotePlugin.price > 0; + helpers + .promisify(iap.getProducts, [remotePlugin.sku]) + .then((products) => { + [product] = products; + if (product) { + return getPurchase(product.productId); + } + return null; + }) + .then((purchase) => { + purchaseToken = purchase?.purchaseToken; + + if (isPaid && !purchaseToken) { + if (!product) throw new Error("Product not found"); + return helpers.checkAPIStatus().then((apiStatus) => { + if (!apiStatus) { + alert(strings.error, strings.api_error); + return; + } + + iap.setPurchaseUpdatedListener( + ...purchaseListener(onpurchase, onerror), + ); + return helpers.promisify( + iap.purchase, + product.json, + ); + }); + } + }) + .then(() => { + import("lib/installPlugin").then( + ({ default: installPlugin }) => { + installPlugin( + pluginId, + remotePlugin.name, + purchaseToken, + ).then(() => { + resolve(); + }); + }, + ); + }); + + async function onpurchase(e) { + const purchase = await getPurchase(product.productId); + await ajax.post( + Url.join(constants.API_BASE, "plugin/order"), + { + data: { + id: remotePlugin.id, + token: purchase?.purchaseToken, + package: BuildInfo.packageName, + }, + }, + ); + purchaseToken = purchase?.purchaseToken; + } + + async function onerror(error) { + throw error; + } + } + }); + + async function getPurchase(sku) { + const purchases = await helpers.promisify(iap.getPurchases); + const purchase = purchases.find((p) => + p.productIds.includes(sku), + ); + return purchase; + } + }); + }) + .catch((error) => { + reject(error); + }); }); } @@ -235,6 +286,7 @@ export default class Acode { if (numFiles) { return strings["unsaved files close app"]; } + return null; } setLoadingMessage(message) { @@ -296,11 +348,11 @@ export default class Acode { (formatter) => formatter.id !== id, ); const { formatter } = appSettings.value; - Object.keys(formatter).forEach((mode) => { + for (const mode of Object.keys(formatter)) { if (formatter[mode] === id) { delete formatter[mode]; } - }); + } appSettings.update(false); } @@ -316,7 +368,8 @@ export default class Acode { formatterSettings(name); this.#afterSelectFormatter(name); return; - } else if (!formatter && !selectIfNull) { + } + if (!formatter && !selectIfNull) { toast(strings["please select a formatter"]); } } @@ -355,12 +408,12 @@ export default class Acode { */ getFormatterFor(extensions) { const options = [[null, strings.none]]; - this.formatters.forEach(({ id, name, exts }) => { + for (const { id, name, exts } of this.formatters) { const supports = exts.some((ext) => extensions.includes(ext)); if (supports || exts.includes("*")) { options.push([id, name]); } - }); + } return options; } @@ -414,8 +467,8 @@ export default class Acode { } async toInternalUrl(url) { - url = await helpers.toInternalUri(url); - return url; + const internalUrl = await helpers.toInternalUri(url); + return internalUrl; } /** * Push a notification diff --git a/src/lib/main.js b/src/lib/main.js index 888a0e15b..0e5f4a2e5 100644 --- a/src/lib/main.js +++ b/src/lib/main.js @@ -110,12 +110,12 @@ async function onDeviceReady() { window.log = logger.log.bind(logger); // Capture synchronous errors - window.addEventListener("error", function (event) { + window.addEventListener("error", (event) => { const errorMsg = `Error: ${event.message}, Source: ${event.filename}, Line: ${event.lineno}, Column: ${event.colno}, Stack: ${event.error?.stack || "N/A"}`; window.log("error", errorMsg); }); // Capture unhandled promise rejections - window.addEventListener("unhandledrejection", function (event) { + window.addEventListener("unhandledrejection", (event) => { window.log( "error", `Unhandled rejection: ${event.reason ? event.reason.message : "Unknown reason"}\nStack: ${event.reason ? event.reason.stack : "No stack available"}`, @@ -161,10 +161,10 @@ async function onDeviceReady() { const $testEl = (
+ /> ); document.body.append($testEl); const client = $testEl.getBoundingClientRect(); @@ -172,7 +172,7 @@ async function onDeviceReady() { $testEl.remove(); if (client.height === 0) return false; - else return true; + return true; })(); window.acode = new Acode(); @@ -239,23 +239,62 @@ async function onDeviceReady() { applySettings.afterRender(); }, 500); } - setTimeout(() => { - checkPluginsUpdate() - .then((updates) => { - if (!updates.length) return; - acode.pushNotification( - "Plugin Updates", - `${updates.length} plugin${updates.length > 1 ? "s" : ""} ${updates.length > 1 ? "have" : "has"} new version${updates.length > 1 ? "s" : ""} available.`, - { - icon: "extension", - action: () => { - plugins(updates); + // Check for app updates + if (navigator.onLine) { + cordova.plugin.http.sendRequest( + "https://api.github.com/repos/Acode-Foundation/Acode/releases/latest", + { + method: "GET", + responseType: "json", + }, + (response) => { + const release = response.data; + // assuming version is in format v1.2.3 + const latestVersion = release.tag_name + .replace("v", "") + .split(".") + .map(Number); + const currentVersion = BuildInfo.version.split(".").map(Number); + + const hasUpdate = latestVersion.some( + (num, i) => num > currentVersion[i], + ); + + if (hasUpdate) { + acode.pushNotification( + "Update Available", + `Acode ${release.tag_name} is now available! Click here to checkout.`, + { + icon: "update", + type: "warning", + action: () => { + system.openInBrowser(release.html_url); + }, }, + ); + } + }, + (err) => { + window.log("error", "Failed to check for updates"); + window.log("error", err); + }, + ); + } + checkPluginsUpdate() + .then((updates) => { + if (!updates.length) return; + acode.pushNotification( + "Plugin Updates", + `${updates.length} plugin${updates.length > 1 ? "s" : ""} ${updates.length > 1 ? "have" : "has"} new version${updates.length > 1 ? "s" : ""} available.`, + { + icon: "extension", + action: () => { + plugins(updates); }, - ); - }) - .catch(console.error); - }, 5000); + }, + ); + }) + .catch(console.error); } async function loadApp() { @@ -269,10 +308,10 @@ async function loadApp() { /> ); const $navToggler = ( - + ); const $menuToggler = ( - + ); const $header = tile({ type: "header", @@ -280,7 +319,7 @@ async function loadApp() { lead: $navToggler, tail: $menuToggler, }); - const $main =
; + const $main =
; const $sidebar = ; const $runBtn = ( acode.exec("run")} oncontextmenu={() => acode.exec("run-file")} - > + /> ); const $floatingNavToggler = ( acode.exec("toggle-sidebar")} - > + /> ); const $headerToggler = ( - + ); const folders = helpers.parseJSON(localStorage.folders); const files = helpers.parseJSON(localStorage.files) || []; @@ -377,7 +413,7 @@ async function loadApp() { setFileMenu(); }); - $sidebar.onshow = function () { + $sidebar.onshow = () => { const activeFile = editorManager.activeFile; if (activeFile) editorManager.editor.blur(); }; @@ -405,10 +441,10 @@ async function loadApp() { acode.setLoadingMessage("Loading folders..."); if (Array.isArray(folders)) { - folders.forEach((folder) => { + for (const folder of folders) { folder.opts.listFiles = !!folder.opts.listFiles; openFolder(folder.url, folder.opts); - }); + } } if (Array.isArray(files) && files.length) { @@ -425,42 +461,6 @@ async function loadApp() { initFileList(); - // Check for app updates - if (navigator.onLine) { - fetch("https://api.github.com/repos/Acode-Foundation/Acode/releases/latest") - .then((res) => res.json()) - .then((release) => { - // assuming version is in format v1.2.3 - const latestVersion = release.tag_name - .replace("v", "") - .split(".") - .map(Number); - const currentVersion = BuildInfo.version.split(".").map(Number); - - const hasUpdate = latestVersion.some( - (num, i) => num > currentVersion[i], - ); - - if (hasUpdate) { - acode.pushNotification( - "Update Available", - `Acode ${release.tag_name} is now available! Click here to checkout.`, - { - icon: "update", - type: "warning", - action: () => { - system.openInBrowser(release.html_url); - }, - }, - ); - } - }) - .catch((err) => { - window.log("error", "Failed to check for updates"); - window.log("error", err); - }); - } - /** * * @param {MouseEvent} e @@ -528,7 +528,7 @@ function onClickApp(e) { function checkIfInsideAnchor() { const allAs = [...document.body.getAll("a")]; - for (let a of allAs) { + for (const a of allAs) { if (a.contains(el)) { el = a; return true; diff --git a/src/sidebarApps/extensions/index.js b/src/sidebarApps/extensions/index.js index 7bd813ad8..6e44c53f3 100644 --- a/src/sidebarApps/extensions/index.js +++ b/src/sidebarApps/extensions/index.js @@ -1,10 +1,12 @@ import "./style.scss"; +import ajax from "@deadlyjack/ajax"; import collapsableList from "components/collapsableList"; import Sidebar from "components/sidebar"; import prompt from "dialogs/prompt"; import select from "dialogs/select"; import fsOperation from "fileSystem"; +import purchaseListener from "handlers/purchase"; import constants from "lib/constants"; import InstallState from "lib/installState"; import settings from "lib/settings"; @@ -30,12 +32,12 @@ let isLoading = false; const $header = (
- {strings["plugins"]} - -
{ + for (const $el of $scrollableLists) { $el.scrollTop = $el.dataset.scrollTop; - }); + } } /** @@ -80,19 +82,19 @@ function initApp(el) { container.content = $header; if (!$searchResult) { - $searchResult =
    ; + $searchResult =
      ; container.append($searchResult); } if (!$explore) { - $explore = collapsableList(strings["explore"]); + $explore = collapsableList(strings.explore); $explore.ontoggle = loadExplore; $explore.$ul.onscroll = handleScroll; container.append($explore); } if (!$installed) { - $installed = collapsableList(strings["installed"]); + $installed = collapsableList(strings.installed); $installed.ontoggle = loadInstalled; //$installed.expand(); container.append($installed); @@ -146,7 +148,7 @@ async function searchPlugin() { const status = helpers.checkAPIStatus(); if (!status) { $searchResult.content = ( - {strings["api_error"]} + {strings.api_error} ); return; } @@ -165,7 +167,7 @@ async function searchPlugin() { updateHeight($searchResult); } catch (error) { window.log("error", error); - $searchResult.content = {strings["error"]}; + $searchResult.content = {strings.error}; } finally { $searchResult.classList.remove("loading"); } @@ -197,7 +199,7 @@ async function filterPlugins() { className="icon clearclose close-button" data-action="clear-filter" onclick={() => clearFilter()} - > + />
    ); $searchResult.content = [filterMessage, ...plugins.map(ListItem)]; @@ -210,7 +212,7 @@ async function filterPlugins() { } catch (error) { window.log("error", "Error filtering plugins:"); window.log("error", error); - $searchResult.content = {strings["error"]}; + $searchResult.content = {strings.error}; } finally { $searchResult.classList.remove("loading"); } @@ -269,9 +271,7 @@ async function loadExplore() { const status = helpers.checkAPIStatus(); if (!status) { - $explore.$ul.content = ( - {strings["api_error"]} - ); + $explore.$ul.content = {strings.api_error}; return; } @@ -294,7 +294,7 @@ async function loadExplore() { currentPage++; updateHeight($explore); } catch (error) { - $explore.$ul.content = {strings["error"]}; + $explore.$ul.content = {strings.error}; } finally { stopLoading($explore); } @@ -383,26 +383,27 @@ function ListItem({ icon, name, id, version, downloads, installed, source }) { } const $el = (
    - + {name} {installed ? ( <> {source ? ( - + ) : null} - + ) : ( - )}
    @@ -421,9 +422,11 @@ function ListItem({ icon, name, id, version, downloads, installed, source }) { if (morePluginActionButton) { more_plugin_action(id, name); return; - } else if (installPluginBtn) { + } + if (installPluginBtn) { try { - let purchaseToken = null; + let purchaseToken; + let product; const pluginUrl = Url.join(constants.API_BASE, `plugin/${id}`); const remotePlugin = await fsOperation(pluginUrl) .readFile("json") @@ -431,30 +434,49 @@ function ListItem({ icon, name, id, version, downloads, installed, source }) { throw new Error("Failed to fetch plugin details"); }); - if (remotePlugin && Number.parseFloat(remotePlugin.price) > 0) { - try { - const [product] = await helpers.promisify(iap.getProducts, [ - remotePlugin.sku, - ]); - if (product) { - async function getPurchase(sku) { - const purchases = await helpers.promisify(iap.getPurchases); - const purchase = purchases.find((p) => - p.productIds.includes(sku), - ); - return purchase; - } - const purchase = await getPurchase(product.productId); - purchaseToken = purchase?.purchaseToken; - } - } catch (error) { - helpers.error(error); - throw new Error("Failed to validate purchase"); + const isPaid = remotePlugin.price > 0; + [product] = await helpers.promisify(iap.getProducts, [ + remotePlugin.sku, + ]); + if (product) { + const purchase = await getPurchase(product.productId); + purchaseToken = purchase?.purchaseToken; + } + + if (isPaid && !purchaseToken) { + if (!product) throw new Error("Product not found"); + const apiStatus = await helpers.checkAPIStatus(); + + if (!apiStatus) { + alert(strings.error, strings.api_error); + return; + } + + iap.setPurchaseUpdatedListener( + ...purchaseListener(onpurchase, onerror), + ); + await helpers.promisify(iap.purchase, product.json); + + async function onpurchase(e) { + const purchase = await getPurchase(product.productId); + await ajax.post(Url.join(constants.API_BASE, "plugin/order"), { + data: { + id: remotePlugin.id, + token: purchase?.purchaseToken, + package: BuildInfo.packageName, + }, + }); + purchaseToken = purchase?.purchaseToken; + } + + async function onerror(error) { + throw error; } } const { default: installPlugin } = await import("lib/installPlugin"); await installPlugin(id, remotePlugin.name, purchaseToken); + const searchInput = container.querySelector('input[name="search-ext"]'); if (searchInput) { searchInput.value = ""; @@ -462,23 +484,31 @@ function ListItem({ icon, name, id, version, downloads, installed, source }) { updateHeight($searchResult); $installed.expand(); } - window.toast(strings["success"], 3000); + + window.toast(strings.success, 3000); if (!$explore.collapsed) { $explore.ontoggle(); } if (!$installed.collapsed) { $installed.ontoggle(); } + + async function getPurchase(sku) { + const purchases = await helpers.promisify(iap.getPurchases); + const purchase = purchases.find((p) => p.productIds.includes(sku)); + return purchase; + } } catch (err) { console.error(err); window.toast(helpers.errorMessage(err), 3000); } return; - } else if (rebuildPluginBtn) { + } + if (rebuildPluginBtn) { try { const { default: installPlugin } = await import("lib/installPlugin"); await installPlugin(source); - window.toast(strings["success"], 3000); + window.toast(strings.success, 3000); } catch (err) { console.error(err); window.toast(helpers.errorMessage(err), 3000); @@ -554,13 +584,13 @@ async function uninstall(id) { async function more_plugin_action(id, pluginName) { let actions; - let pluginSettings = settings.uiSettings[`plugin-${id}`]; + const pluginSettings = settings.uiSettings[`plugin-${id}`]; if (pluginSettings) { actions = [strings.settings, strings.uninstall]; } else { actions = [strings.uninstall]; } - let action = await select("Action", actions); + const action = await select("Action", actions); if (!action) return; switch (action) { case strings.settings: