diff --git a/src/lib/installPlugin.js b/src/lib/installPlugin.js index 5488d9131..7695c226c 100644 --- a/src/lib/installPlugin.js +++ b/src/lib/installPlugin.js @@ -82,23 +82,23 @@ export default async function installPlugin( }, ); } else { - // cordova http plugin for others - plugin = await new Promise((resolve, reject) => { - cordova.plugin.http.sendRequest( + const tempPath = cordova.file.cacheDirectory + "plugin_download.zip"; + + await new Promise((resolve, reject) => { + cordova.exec(resolve, reject, "Authenticator", "downloadPlugin", [ pluginUrl, - { - method: "GET", - responseType: "arraybuffer", - }, - (response) => { - resolve(response.data); - loaderDialog.setMessage(`${strings.loading} 100%`); - }, - (error) => { - reject(error); - }, - ); + tempPath, + ]); }); + + plugin = await fsOperation(tempPath).readFile( + undefined, + (loaded, total) => { + loaderDialog.setMessage( + `${strings.loading} ${((loaded / total) * 100).toFixed(2)}%`, + ); + }, + ); } if (plugin) { diff --git a/src/pages/plugin/plugin.js b/src/pages/plugin/plugin.js index 56f7f7f86..4465e4214 100644 --- a/src/pages/plugin/plugin.js +++ b/src/pages/plugin/plugin.js @@ -168,9 +168,11 @@ export default async function PluginInclude( ]); if (product) { const purchase = await getPurchase(product.productId); - purchased = !!purchase; + purchased = !!purchase || remotePlugin.owned; price = product.price; purchaseToken = purchase?.purchaseToken; + } else if (remotePlugin.owned) { + purchased = true; } } catch (error) { helpers.error(error); diff --git a/src/pages/plugins/plugins.js b/src/pages/plugins/plugins.js index d16aa2dcc..9e6154a51 100644 --- a/src/pages/plugins/plugins.js +++ b/src/pages/plugins/plugins.js @@ -416,95 +416,82 @@ export default function PluginsInclude(updates) { } } - async function retrieveFilteredPlugins(filterState) { + //fetch plugins with the auth token + function fetchPlugins(url) { + return new Promise((resolve, reject) => { + cordova.exec( + (items) => resolve(items), + (err) => reject(new Error(err)), + "Authenticator", + "fetchPlugins", + [url] + ); + }); +} + +async function retrieveFilteredPlugins(filterState) { if (!filterState) return { items: [], hasMore: false }; if (filterState.type === "orderBy") { - const page = filterState.nextPage || 1; - try { - let response; - if (filterState.value === "top_rated") { - response = await fetch( - withSupportedEditor( - `${constants.API_BASE}/plugins?explore=random&page=${page}&limit=${LIMIT}`, - ), - ); - } else { - response = await fetch( - withSupportedEditor( - `${constants.API_BASE}/plugin?orderBy=${filterState.value}&page=${page}&limit=${LIMIT}`, - ), - ); - } - const items = await response.json(); - if (!Array.isArray(items)) { - return { items: [], hasMore: false }; + const page = filterState.nextPage || 1; + try { + let url; + if (filterState.value === "top_rated") { + url = withSupportedEditor(`${constants.API_BASE}/plugins?explore=random&page=${page}&limit=${LIMIT}`); + } else { + url = withSupportedEditor(`${constants.API_BASE}/plugin?orderBy=${filterState.value}&page=${page}&limit=${LIMIT}`); + } + + const items = await fetchPlugins(url); + filterState.nextPage = page + 1; + return { items, hasMore: items.length === LIMIT }; + } catch (error) { + console.error("Failed to fetch ordered plugins:", error); + return { items: [], hasMore: false }; } - filterState.nextPage = page + 1; - const hasMoreResults = items.length === LIMIT; - return { items, hasMore: hasMoreResults }; - } catch (error) { - console.error("Failed to fetch ordered plugins:", error); - return { items: [], hasMore: false }; - } } - if (!Array.isArray(filterState.buffer)) { - filterState.buffer = []; - } - if (filterState.hasMoreSource === undefined) { - filterState.hasMoreSource = true; - } - if (!filterState.nextPage) { - filterState.nextPage = 1; - } + if (!Array.isArray(filterState.buffer)) filterState.buffer = []; + if (filterState.hasMoreSource === undefined) filterState.hasMoreSource = true; + if (!filterState.nextPage) filterState.nextPage = 1; const items = []; while (items.length < LIMIT) { - if (filterState.buffer.length) { - items.push(filterState.buffer.shift()); - continue; - } + if (filterState.buffer.length) { + items.push(filterState.buffer.shift()); + continue; + } - if (filterState.hasMoreSource === false) break; + if (filterState.hasMoreSource === false) break; - try { - const page = filterState.nextPage; - const response = await fetch( - withSupportedEditor(`${constants.API_BASE}/plugins?page=${page}&limit=${LIMIT}`), - ); - const data = await response.json(); - filterState.nextPage = page + 1; + try { + const url = withSupportedEditor(`${constants.API_BASE}/plugins?page=${filterState.nextPage}&limit=${LIMIT}`); + const data = await fetchPlugins(url); // <-- java call + filterState.nextPage++; - if (!Array.isArray(data) || !data.length) { - filterState.hasMoreSource = false; - break; - } + if (!Array.isArray(data) || !data.length) { + filterState.hasMoreSource = false; + break; + } - if (data.length < LIMIT) { - filterState.hasMoreSource = false; - } + if (data.length < LIMIT) filterState.hasMoreSource = false; - const matched = data.filter((plugin) => matchesFilter(plugin, filterState)); - filterState.buffer.push(...matched); - } catch (error) { - console.error("Failed to fetch filtered plugins:", error); - filterState.hasMoreSource = false; - break; - } + filterState.buffer.push(...data.filter(plugin => matchesFilter(plugin, filterState))); + } catch (error) { + console.error("Failed to fetch filtered plugins:", error); + filterState.hasMoreSource = false; + break; + } } while (items.length < LIMIT && filterState.buffer.length) { - items.push(filterState.buffer.shift()); + items.push(filterState.buffer.shift()); } - const hasMoreResults = - (filterState.hasMoreSource !== false && filterState.nextPage) || - filterState.buffer.length > 0; - + const hasMoreResults = (filterState.hasMoreSource !== false && filterState.nextPage) || filterState.buffer.length > 0; return { items, hasMore: Boolean(hasMoreResults) }; - } +} function matchesFilter(plugin, filterState) { if (!plugin) return false; @@ -559,6 +546,7 @@ export default function PluginsInclude(updates) { return []; } + //auth token is not needed here as this endpoint only returns public plugins and the server handles the filtering based on supported editor async function getAllPlugins() { if (currentFilter) return; if (isLoading || !hasMore) return; @@ -631,17 +619,68 @@ export default function PluginsInclude(updates) { ); $list.installed.setAttribute("empty-msg", strings["no plugins found"]); } +async function getOwned() { + $list.owned.setAttribute("empty-msg", strings["loading..."]); - async function getOwned() { - $list.owned.setAttribute("empty-msg", strings["loading..."]); + const disabledMap = settings.value.pluginsDisabled || {}; + const addedIds = new Set(); + + // ------------------- + // Google Play / IAP + // ------------------- + try { const purchases = await helpers.promisify(iap.getPurchases); - const disabledMap = settings.value.pluginsDisabled || {}; - purchases.forEach(async ({ productIds }) => { - const [sku] = productIds; - const url = Url.join(constants.API_BASE, "plugin/owned", sku); - const plugin = await fsOperation(url).readFile("json"); - const isInstalled = plugins.installed.find(({ id }) => id === plugin.id); + await Promise.all( + purchases.map(async ({ productIds }) => { + const [sku] = productIds; + + try { + const url = Url.join(constants.API_BASE, "plugin/owned", sku); + const plugin = await fsOperation(url).readFile("json"); + + if (!plugin || addedIds.has(plugin.id)) return; + + const isInstalled = plugins.installed.find( + ({ id }) => id === plugin.id + ); + + plugin.installed = !!isInstalled; + + if (plugin.installed) { + plugin.enabled = disabledMap[plugin.id] !== true; + plugin.onToggleEnabled = onToggleEnabled; + } + + addedIds.add(plugin.id); + plugins.owned.push(plugin); + $list.owned.append(); + } catch (err) { + console.error("Failed to load owned IAP plugin:", err); + } + }) + ); + } catch (err) { + console.warn("IAP unavailable", err); + } + + // ------------------- + // Razorpay purchases + // ------------------- + try { + const url = withSupportedEditor( + `${constants.API_BASE}/plugins?owned=true` + ); + + const ownedPlugins = await fetchPlugins(url); + + ownedPlugins.forEach((plugin) => { + if (!plugin || addedIds.has(plugin.id)) return; + + const isInstalled = plugins.installed.find( + ({ id }) => id === plugin.id + ); + plugin.installed = !!isInstalled; if (plugin.installed) { @@ -649,12 +688,16 @@ export default function PluginsInclude(updates) { plugin.onToggleEnabled = onToggleEnabled; } + addedIds.add(plugin.id); plugins.owned.push(plugin); $list.owned.append(); }); - $list.owned.setAttribute("empty-msg", strings["no plugins found"]); + } catch (err) { + console.error("Failed to fetch owned plugins:", err); } + $list.owned.setAttribute("empty-msg", strings["no plugins found"]); +} function onInstall(plugin) { if (updates) return; diff --git a/src/plugins/auth/plugin.xml b/src/plugins/auth/plugin.xml index e6e3e448c..284e43d89 100644 --- a/src/plugins/auth/plugin.xml +++ b/src/plugins/auth/plugin.xml @@ -13,6 +13,7 @@ + diff --git a/src/plugins/auth/src/android/Authenticator.java b/src/plugins/auth/src/android/Authenticator.java index 48bf3cc08..51028f18e 100644 --- a/src/plugins/auth/src/android/Authenticator.java +++ b/src/plugins/auth/src/android/Authenticator.java @@ -8,9 +8,9 @@ import java.net.HttpURLConnection; import java.net.URL; import java.util.Scanner; +import org.json.JSONObject; public class Authenticator extends CordovaPlugin { - // Standard practice: use a TAG for easy filtering in Logcat private static final String TAG = "AcodeAuth"; private static final String PREFS_FILENAME = "acode_auth_secure"; private static final String KEY_TOKEN = "auth_token"; @@ -42,6 +42,33 @@ public boolean execute(String action, JSONArray args, CallbackContext callbackCo prefManager.setString(KEY_TOKEN, token); callbackContext.success(); return true; + case "downloadPlugin": + cordova.getThreadPool().execute(() -> { + try { + PluginRetriever.downloadPlugin( + prefManager.getString(KEY_TOKEN, null), + args.getString(0), + args.getString(1), + callbackContext + ); + } catch (Exception e) { + Log.e(TAG, "downloadPlugin error: " + e.getMessage(), e); + callbackContext.error("Error: " + e.getMessage()); + } + }); + return true; + case "fetchPlugins": + cordova.getThreadPool().execute(() -> { + try { + String url = args.getString(0); + JSONArray items = PluginRetriever.fetchJsonArray(url, prefManager.getString(KEY_TOKEN, null)); + callbackContext.success(items != null ? items : new JSONArray()); + } catch (Exception e) { + Log.e(TAG, "fetchPlugins error: " + e.getMessage(), e); + callbackContext.error("Error: " + e.getMessage()); + } + }); + return true; default: Log.w(TAG, "Attempted to call unknown action: " + action); return false; @@ -114,12 +141,12 @@ private String validateToken(String token) { HttpURLConnection conn = null; try { Log.d(TAG, "Network Request: Connecting to https://acode.app/api/login"); - URL url = new URL("https://acode.app/api/login"); // Changed from /api to /api/login + URL url = new URL("https://acode.app/api/login"); conn = (HttpURLConnection) url.openConnection(); conn.setRequestProperty("x-auth-token", token); conn.setRequestMethod("GET"); conn.setConnectTimeout(5000); - conn.setReadTimeout(5000); // Add read timeout too + conn.setReadTimeout(5000); int code = conn.getResponseCode(); Log.d(TAG, "Server responded with status code: " + code); diff --git a/src/plugins/auth/src/android/PluginRetriever.java b/src/plugins/auth/src/android/PluginRetriever.java new file mode 100644 index 000000000..6d4d2f929 --- /dev/null +++ b/src/plugins/auth/src/android/PluginRetriever.java @@ -0,0 +1,130 @@ +package com.foxdebug.acode.rk.auth; + +import android.util.Log; +import org.apache.cordova.CallbackContext; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Scanner; +import android.content.Context; +import java.io.File; +import java.io.InputStream; +import java.io.FileOutputStream; + +public class PluginRetriever { + private static final String TAG = "AcodePluginRetriever"; + private static final String SUPPORTED_EDITOR = "cm"; + + + private static String withSupportedEditor(String url) { + String separator = url.contains("?") ? "&" : "?"; + return url + separator + "supported_editor=" + SUPPORTED_EDITOR; + } + + + + public static void downloadPlugin(String token, String pluginUrl, String destFile, CallbackContext callbackContext) { + HttpURLConnection connection = null; + + try { + // Strip file:// prefix if present + if (destFile.startsWith("file://")) { + destFile = destFile.substring(7); + } + + URL url = new URL(pluginUrl); + connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + connection.setConnectTimeout(15000); + connection.setReadTimeout(30000); + + if (token != null && !token.isEmpty()) { + String host = url.getHost(); + + if (host != null && + (host.equals("acode.app") || host.endsWith(".acode.app"))) { + connection.setRequestProperty("x-auth-token", token); + }else { + Log.w(TAG, "Not adding auth token for untrusted URL: " + url); + } + } + + connection.connect(); + + int responseCode = connection.getResponseCode(); + if (responseCode != HttpURLConnection.HTTP_OK) { + callbackContext.error("HTTP error: " + responseCode); + return; + } + + File tempFile = new File(destFile); + + try ( + InputStream inputStream = connection.getInputStream(); + FileOutputStream outputStream = new FileOutputStream(tempFile) + ) { + byte[] buffer = new byte[4096]; + int bytesRead; + + while ((bytesRead = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + } + } + + callbackContext.success(); + + } catch (Exception e) { + callbackContext.error("Download failed: " + e.getMessage()); + + } finally { + if (connection != null) { + connection.disconnect(); + } + } + } + + + public static JSONArray fetchJsonArray(String urlString, String token) { + HttpURLConnection conn = null; + try { + Log.d(TAG, "Fetching: " + urlString); + URL url = new URL(urlString); + conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + conn.setConnectTimeout(5000); + conn.setReadTimeout(5000); + + if (token != null && !token.isEmpty()) { + String host = url.getHost(); + + if (host != null && + (host.equals("acode.app") || host.endsWith(".acode.app"))) { + conn.setRequestProperty("x-auth-token", token); + }else { + Log.w(TAG, "Not adding auth token for untrusted URL: " + url); + } + } + + int code = conn.getResponseCode(); + if (code != 200) { + Log.w(TAG, "Non-200 response (" + code + ") for: " + urlString); + return null; + } + + Scanner s = new Scanner(conn.getInputStream(), "UTF-8").useDelimiter("\\A"); + String body = s.hasNext() ? s.next() : "[]"; + s.close(); + return new JSONArray(body); + } catch (Exception e) { + Log.e(TAG, "fetchJsonArray error: " + e.getMessage(), e); + return null; + } finally { + if (conn != null) conn.disconnect(); + } + } + + + +} \ No newline at end of file