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