+
+ Paid plugin support has been discontinued. Plugins submitted with a price will be rejected.
+
+
+
{mode === 'update' && (
@@ -156,10 +162,6 @@ export default async function PublishPlugin({ mode = 'publish', id }) {
Min Version Code
{minVersionCode}
-
-
Price
-
{pluginPrice}
-
Icon
{pluginIcon}
@@ -208,7 +210,6 @@ export default async function PublishPlugin({ mode = 'publish', id }) {
pluginName.value = '';
pluginVersion.value = '';
pluginAuthor.value = '';
- pluginPrice.value = '';
pluginIcon.src = '#';
minVersionCode.value = '';
submitButton.el.disabled = true;
@@ -274,12 +275,6 @@ export default async function PublishPlugin({ mode = 'publish', id }) {
updateType.value = `(${getUpdateType(manifest.version, plugin.version)} from ${plugin.version})`;
}
- if (+manifest.price) {
- pluginPrice.value = `INR ${manifest.price || 0}`;
- } else {
- pluginPrice.value = 'Free';
- }
-
pluginId.value = manifest.id;
pluginName.value = manifest.name;
pluginVersion.value = manifest.version;
diff --git a/client/pages/publishPlugin/style.scss b/client/pages/publishPlugin/style.scss
index f594994..994609a 100644
--- a/client/pages/publishPlugin/style.scss
+++ b/client/pages/publishPlugin/style.scss
@@ -1,4 +1,26 @@
#publish-plugin {
+ .paid-plugin-notice.update-banner {
+ background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
+ box-shadow:
+ 0 4px 24px rgba(245, 158, 11, 0.25),
+ 0 8px 48px rgba(217, 119, 6, 0.2);
+ margin-bottom: 16px;
+
+ &::after {
+ content: "\e915";
+ }
+
+ .icon-wrapper .icon {
+ color: rgba(255, 255, 255, 0.95);
+ }
+
+ span {
+ color: #fff;
+ font-size: 15px;
+ font-weight: 500;
+ }
+ }
+
.update-banner {
position: relative;
margin: 20px auto 30px;
diff --git a/client/pages/termsOfService/website-ts.md b/client/pages/termsOfService/website-ts.md
index bceed2c..eea5b08 100644
--- a/client/pages/termsOfService/website-ts.md
+++ b/client/pages/termsOfService/website-ts.md
@@ -16,6 +16,23 @@ You retain ownership of any plugins you upload to our website. However, by uploa
We will pay you for your plugin sales and download count. Payments will be made to the bank account or PayPal email address you provide as your default payment method. If your earnings from the previous month(s) sum up to an amount equal to or greater than the threshold, your payment will be automatically transferred to your default payment method on the 16th of each month. If the 16th falls on a weekend, the transfer will occur on the next working day. Please note that it may take up to 5 working days for the amount to be credited to your account. You are responsible for any taxes or fees associated with receiving payments.
+## Paid Plugin Discontinuation
+
+Effective **6 April 2026**, paid plugin support has been discontinued. All previously approved paid plugins have been deactivated from the Acode plugin store.
+
+**For plugin developers:**
+
+- You can restore your plugin to the store at no cost by visiting your plugin page and clicking **"Make Free."**
+- Once made free, your plugin will be immediately available to all Acode users again.
+
+**Compensation for pending earnings:**
+
+- Developers with outstanding unpaid earnings from paid plugin sales will be compensated in full.
+- International developers will receive an Amazon gift card of equivalent value to their total pending earnings.
+- If you have pending earnings, please ensure your account email is up to date and contact us at [contact@acode.app](mailto:contact@acode.app) to initiate your compensation.
+
+We sincerely apologize for any inconvenience this may cause and thank you for your continued support of the Acode community.
+
## Payment Threshold
The payment threshold is the minimum amount of earnings required for a payment to be processed and transferred to your designated payment method. Different minimum thresholds apply depending on the chosen payment method:
diff --git a/client/pages/user/index.js b/client/pages/user/index.js
index 97205fc..f5f312c 100644
--- a/client/pages/user/index.js
+++ b/client/pages/user/index.js
@@ -52,7 +52,6 @@ export default async function User({ userId }) {
if (shouldShowSensitiveInfo) {
renderEarnings();
- renderPaymentMethods();
}
return (
@@ -76,16 +75,6 @@ export default async function User({ userId }) {
) : (
''
)}
-
- {isSelf ? (
-
-
- Payment method
-
- ) : (
- ''
- )}
-
e.target.dataset.href && Router.loadUrl(e.target.dataset.href)}>
{shouldShowSensitiveInfo && }
{user.github && (
@@ -102,15 +91,6 @@ export default async function User({ userId }) {
);
- /**
- * Scroll payment methods horizontally on mouse wheel
- * @param {WheelEvent} e
- */
- function onwheel(e) {
- e.preventDefault();
- this.scrollLeft += e.deltaY;
- }
-
function PaymentMethod({
id,
paypal_email: paypalEmail,
@@ -203,14 +183,6 @@ export default async function User({ userId }) {
}
}
- /**
- * Add payment method
- */
- async function addPaymentMethod() {
- const option = await select('Add payment method', ['Paypal', 'Bank account', 'Crypto']);
- Router.loadUrl(`/add-payment-method/${option.toLowerCase().replace(' ', '-')}`);
- }
-
async function renderPaymentMethods() {
showLoading();
try {
diff --git a/readme.md b/readme.md
index 10bba32..57b41e2 100644
--- a/readme.md
+++ b/readme.md
@@ -54,8 +54,8 @@ This project is licensed under the MIT License. See the [LICENSE](LICENSE) file
## Contact
-For any inquiries or support, please visit our [contact page](https://acode.app/contact) or reach out via [email](mailto:support@acode.app).
+For any inquiries or support, please visit our [contact page](https://acode.app/contact) or reach out via [email](mailto:contact@acode.app).
---
-*Empower your coding journey with Acode—code anytime, anywhere.*
+_Empower your coding journey with Acode—code anytime, anywhere._
diff --git a/server/apis/admin.js b/server/apis/admin.js
index 995a7ba..d9d6abb 100644
--- a/server/apis/admin.js
+++ b/server/apis/admin.js
@@ -116,4 +116,35 @@ router.delete('/user/:id', async (req, res) => {
res.send(user);
});
+const ALLOWED_FILTERS = ['all', 'with_plugins', 'with_paid_plugins', 'with_payment'];
+
+router.get('/email-recipients-count', async (req, res) => {
+ const { filter = 'all' } = req.query;
+ if (!ALLOWED_FILTERS.includes(filter)) {
+ res.status(400).send({ error: 'Invalid filter' });
+ return;
+ }
+ const count = await User.countUsersByFilter(filter);
+ res.send({ count });
+});
+
+router.post('/send-email', async (req, res) => {
+ const { filter = 'all', subject, message } = req.body;
+ if (!ALLOWED_FILTERS.includes(filter)) {
+ res.status(400).send({ error: 'Invalid filter' });
+ return;
+ }
+ if (!subject?.trim() || !message?.trim()) {
+ res.status(400).send({ error: 'Subject and message are required' });
+ return;
+ }
+ const users = await User.getUsersByFilter(filter);
+ for (const user of users) {
+ sendEmail(user.email, user.name, subject.trim(), message.trim()).catch((err) => {
+ console.error(`Failed to send email to ${user.email}:`, err);
+ });
+ }
+ res.send({ sent: users.length });
+});
+
module.exports = router;
diff --git a/server/apis/plugin.js b/server/apis/plugin.js
index c84c1cd..d23723e 100644
--- a/server/apis/plugin.js
+++ b/server/apis/plugin.js
@@ -15,8 +15,6 @@ const sendEmail = require('../lib/sendEmail');
const androidpublisher = google.androidpublisher('v3');
const router = Router();
-const MIN_PRICE = 10;
-const MAX_PRICE = 10000;
const VERSION_REGEX = /^\d+\.\d+\.\d+$/;
const ID_REGEX = /^[a-z][a-z0-9._]{3,49}$/i;
const validLicenses = [
@@ -244,14 +242,21 @@ router.get('{/:pluginId}', async (req, res) => {
columns.push(Plugin.AUTHOR_GITHUB);
}
- if (loggedInUser && (loggedInUser.isAdmin || loggedInUser.id === userId)) {
+ if (loggedInUser && (loggedInUser.isAdmin || loggedInUser.id === userId || !!pluginId)) {
columns.push(Plugin.STATUS);
}
- if (!loggedInUser) {
- where.push([Plugin.STATUS, Plugin.STATUS_APPROVED]);
- } else if (loggedInUser.id === userId && !loggedInUser.isAdmin) {
- where.push([Plugin.STATUS, Plugin.STATUS_INACTIVE, '<>']);
+ if (pluginId) {
+ // Single plugin fetch: apply status filter only for anonymous users;
+ // logged-in users get all statuses, access is checked after fetch.
+ if (!loggedInUser) {
+ where.push([Plugin.STATUS, Plugin.STATUS_APPROVED]);
+ }
+ } else {
+ // List mode: owner and admin see all statuses; everyone else sees only approved.
+ if (!loggedInUser || (!loggedInUser.isAdmin && loggedInUser.id !== userId)) {
+ where.push([Plugin.STATUS, Plugin.STATUS_APPROVED]);
+ }
}
if (pluginId) {
@@ -309,6 +314,13 @@ router.get('{/:pluginId}', async (req, res) => {
return;
}
+ // Non-owner, non-admin users can only see approved plugins.
+ const isOwner = loggedInUser && loggedInUser.id === row.user_id;
+ if (row.status !== 'approved' && !isOwner && !loggedInUser?.isAdmin) {
+ res.status(404).send({ error: 'Not found' });
+ return;
+ }
+
res.send(row);
return;
}
@@ -415,15 +427,11 @@ router.post('/', async (req, res) => {
return;
}
- if (price) {
- if (price < MIN_PRICE || price > MAX_PRICE) {
- res.status(400).send({
- error: `Price should be between INR ${MIN_PRICE} and INR ${MAX_PRICE}`,
- });
- return;
- }
-
- await registerSKU(name, pluginId, price);
+ if (price > 0) {
+ res.status(400).send({
+ error: 'Paid plugins are no longer supported. Please set price to 0 and resubmit your plugin as free.',
+ });
+ return;
}
const insert = [
@@ -578,8 +586,11 @@ router.put('/', async (req, res) => {
}
if (row.price !== price) {
- if (price) {
- await registerSKU(name, pluginId, price);
+ if (price > 0) {
+ res.status(400).send({
+ error: 'Paid plugins are no longer supported. Please set price to 0.',
+ });
+ return;
}
updates.push([Plugin.PRICE, price]);
}
@@ -596,6 +607,44 @@ router.put('/', async (req, res) => {
}
});
+router.patch('/:id/make-free', async (req, res) => {
+ try {
+ const { id } = req.params;
+ const user = await getLoggedInUser(req);
+
+ if (!user) {
+ res.status(401).send({ error: 'Unauthorized' });
+ return;
+ }
+
+ const [plugin] = await Plugin.get([Plugin.ID, Plugin.USER_ID, Plugin.PRICE, Plugin.STATUS], [Plugin.ID, id]);
+ if (!plugin) {
+ res.status(404).send({ error: 'Plugin not found' });
+ return;
+ }
+
+ if (plugin.user_id !== user.id && !user.isAdmin) {
+ res.status(403).send({ error: 'Forbidden' });
+ return;
+ }
+
+ if (!plugin.price || plugin.price <= 0) {
+ res.status(400).send({ error: 'Plugin is already free' });
+ return;
+ }
+
+ const updates = [[Plugin.PRICE, 0]];
+ if (plugin.status === 'deleted') {
+ updates.push([Plugin.STATUS, Plugin.STATUS_APPROVED]);
+ }
+
+ await Plugin.update(updates, [Plugin.ID, id]);
+ res.send({ message: 'Plugin is now free and has been restored to the store.' });
+ } catch (error) {
+ res.status(500).send({ error: error.message });
+ }
+});
+
router.patch('/', async (req, res) => {
try {
const { id, status, reason } = req.body;
@@ -852,79 +901,6 @@ function validatePlugin(json, icon, readmeFile) {
return null;
}
-/**
- * Create a in-app product
- * @param {string} package
- * @param {string} name
- * @param {string} id
- * @param {number} price
- */
-async function registerSKU(name, id, price) {
- const sku = getPluginSKU(id);
- if (!isValidPrice(price)) {
- throw new Error('Invalid price');
- }
-
- await register('com.foxdebug.acode');
- await register('com.foxdebug.acodefree');
- async function register(packageName) {
- try {
- const requestBody = {
- sku,
- packageName,
- status: 'active',
- defaultPrice: {
- currency: 'INR',
- priceMicros: price * 1000000,
- },
- defaultLanguage: 'en-US',
- purchaseType: 'managedUser',
- listings: {
- 'en-US': {
- title: name,
- description: `Purchase ${name} (${id}) plugin for Acode editor`,
- },
- },
- };
-
- let skuAlreadyExists = false;
-
- try {
- await androidpublisher.inappproducts.get({
- sku,
- packageName,
- });
- skuAlreadyExists = true;
- } catch (_error) {
- // SKU does not exist
- }
-
- if (skuAlreadyExists) {
- await androidpublisher.inappproducts.update({
- sku,
- packageName,
- requestBody,
- autoConvertMissingPrices: true,
- });
- return;
- }
-
- await androidpublisher.inappproducts.insert({
- packageName,
- requestBody,
- autoConvertMissingPrices: true,
- });
- } catch (error) {
- const message = error.errors?.map(({ message: msg }) => msg).join('\n');
- throw new Error(`Failed to register SKU, ${message}`);
- }
- }
-}
-
-function isValidPrice(price) {
- return price && !Number.isNaN(price) && price >= MIN_PRICE && price <= MAX_PRICE;
-}
-
function isVersionGreater(newV, oldV) {
const [newMajor, newMinor, newPatch] = newV.split('.').map(Number);
const [oldMajor, oldMinor, oldPatch] = oldV.split('.').map(Number);
diff --git a/server/entities/user.js b/server/entities/user.js
index 09a1042..3cae8f9 100644
--- a/server/entities/user.js
+++ b/server/entities/user.js
@@ -67,6 +67,55 @@ class User extends Entity {
return super.delete(where, operator);
}
+ getUsersByFilter(filter) {
+ let sql;
+ switch (filter) {
+ case 'with_plugins':
+ sql = `SELECT DISTINCT u.name, u.email FROM user u
+ INNER JOIN plugin p ON u.id = p.user_id
+ WHERE u.role != 'admin' AND p.status != 3`;
+ break;
+ case 'with_paid_plugins':
+ sql = `SELECT DISTINCT u.name, u.email FROM user u
+ INNER JOIN plugin p ON u.id = p.user_id
+ WHERE u.role != 'admin' AND p.price > 0 AND p.status != 3`;
+ break;
+ case 'with_payment':
+ sql = `SELECT DISTINCT u.name, u.email FROM user u
+ INNER JOIN payment pay ON u.id = pay.user_id
+ WHERE u.role != 'admin' AND pay.status = 1`;
+ break;
+ default:
+ sql = `SELECT name, email FROM user WHERE role != 'admin'`;
+ }
+ return Entity.execSql(sql, [], this);
+ }
+
+ async countUsersByFilter(filter) {
+ let sql;
+ switch (filter) {
+ case 'with_plugins':
+ sql = `SELECT COUNT(DISTINCT u.id) as count FROM user u
+ INNER JOIN plugin p ON u.id = p.user_id
+ WHERE u.role != 'admin' AND p.status != 3`;
+ break;
+ case 'with_paid_plugins':
+ sql = `SELECT COUNT(DISTINCT u.id) as count FROM user u
+ INNER JOIN plugin p ON u.id = p.user_id
+ WHERE u.role != 'admin' AND p.price > 0 AND p.status != 3`;
+ break;
+ case 'with_payment':
+ sql = `SELECT COUNT(DISTINCT u.id) as count FROM user u
+ INNER JOIN payment pay ON u.id = pay.user_id
+ WHERE u.role != 'admin' AND pay.status = 1`;
+ break;
+ default:
+ sql = `SELECT COUNT(*) as count FROM user WHERE role != 'admin'`;
+ }
+ const [{ count }] = await Entity.execSql(sql, [], this);
+ return count;
+ }
+
get columns() {
return [
this.ID,
diff --git a/server/migrations/deactivatePaidPlugins.js b/server/migrations/deactivatePaidPlugins.js
new file mode 100644
index 0000000..71c5761
--- /dev/null
+++ b/server/migrations/deactivatePaidPlugins.js
@@ -0,0 +1,16 @@
+/**
+ * One-time migration: deactivate all approved paid plugins.
+ * Run once with: node server/migrations/deactivatePaidPlugins.js
+ */
+const db = require('../lib/db');
+
+const stmt = db.prepare(
+ `UPDATE plugin
+ SET status = 3,
+ status_change_message = 'Paid plugin support has been discontinued. Visit your plugin page to make it free and restore it.'
+ WHERE price > 0 AND status = 1`,
+);
+
+const result = stmt.run();
+console.log(`Deactivated ${result.changes} paid plugin(s).`);
+process.exit(0);