This commit is contained in:
Javier Casares 2026-08-17 16:03:39 +00:00
commit 0e617fa428
12 changed files with 286 additions and 259 deletions

View file

@ -64,13 +64,78 @@ class Robotstxt_Manager_Admin {
wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'robotstxt-manager' ) );
}
$client = Robotstxt_Manager_Core_Client::from_options();
$catalog = $client->get_catalog();
$local = $this->resolve_local_state( $catalog );
$client = Robotstxt_Manager_Core_Client::from_options();
$catalog = $client->get_catalog();
$local = $this->resolve_local_state( $catalog );
$subscriptions = $client->get_subscriptions();
// Admin notices for subscriptions needing attention.
$manager_notices = $this->build_subscription_notices( $catalog, $subscriptions );
$manager_subscriptions = $subscriptions;
require ROBOTSTXT_MANAGER_DIR . 'admin/views/page-catalog.php';
}
/**
* Builds admin notices for subscriptions that need attention.
*
* - payment_failed: persistent warning per plugin.
* - expiring within 14 days: per-plugin warning.
* - expired while the plugin is still installed+active: per-plugin warning.
*
* @param list<array<string, mixed>> $catalog Catalog entries.
* @param array<string, array<string, mixed>> $subscriptions Rows keyed by slug.
*
* @return list<array{type:string, message:string}>
*/
private function build_subscription_notices( array $catalog, array $subscriptions ): array {
$notices = array();
$local = $this->resolve_local_state( $catalog );
foreach ( $subscriptions as $slug => $sub ) {
$raw_status = $sub['status'] ?? '';
$raw_expires_at = $sub['expires_at'] ?? '';
$status = is_string( $raw_status ) ? $raw_status : '';
$expires_at = is_string( $raw_expires_at ) ? $raw_expires_at : '';
if ( 'payment_failed' === $status ) {
$notices[] = array(
'type' => 'warning',
/* translators: %s: plugin slug. */
'message' => sprintf( __( 'The payment for %s failed. Update your payment method from your ROBOTSTXT account page to keep access.', 'robotstxt-manager' ), $slug ),
);
continue;
}
if ( 'expired' === $status ) {
$state = $local[ $slug ] ?? array();
if ( ! empty( $state['active'] ) ) {
$notices[] = array(
'type' => 'warning',
/* translators: %s: plugin slug. */
'message' => sprintf( __( 'The subscription for %s has expired, but the plugin is still active on this site. Renew from your ROBOTSTXT account page to keep receiving updates.', 'robotstxt-manager' ), $slug ),
);
}
continue;
}
if ( 'active' === $status && '' !== $expires_at ) {
$days = (int) floor( ( (int) strtotime( $expires_at ) - time() ) / DAY_IN_SECONDS );
if ( $days >= 0 && $days <= 14 ) {
$notices[] = array(
'type' => 'warning',
/* translators: 1: plugin slug, 2: days remaining. */
'message' => sprintf( _n( 'The subscription for %1$s expires in %2$d day.', 'The subscription for %1$s expires in %2$d days.', $days, 'robotstxt-manager' ), $slug, $days ),
);
}
}
}
return $notices;
}
/**
* Handles the "Refresh catalog" admin-post action.
*
@ -100,6 +165,7 @@ class Robotstxt_Manager_Admin {
$client = Robotstxt_Manager_Core_Client::from_options();
$client->clear_catalog_cache();
$client->clear_subscriptions_cache();
delete_site_transient( 'update_plugins' );
$redirect = add_query_arg(

View file

@ -348,6 +348,7 @@ class Robotstxt_Manager_Settings {
}
delete_transient( 'robotstxt_manager_catalog' );
delete_transient( 'robotstxt_manager_subscriptions' );
return Robotstxt_Manager_Encryption::encrypt( $plain );
}
@ -406,6 +407,7 @@ class Robotstxt_Manager_Settings {
delete_option( 'robotstxt_manager_api_key' );
delete_transient( 'robotstxt_manager_catalog' );
delete_transient( 'robotstxt_manager_subscriptions' );
wp_send_json_success(
array(

View file

@ -7,6 +7,10 @@
* (website link + description), an intro paragraph, and Support /
* Payments sections below the table.
*
* Also available:
* $manager_subscriptions array<string, array{status:string, expires_at:string}>
* Account subscriptions keyed by slug (may be empty).
*
* @package Robotstxt_Manager
*/
@ -19,6 +23,8 @@ if ( ! defined( 'ABSPATH' ) ) {
*
* @var list<array<string,mixed>> $catalog
* @var array<string, array{installed:bool, active:bool, version:string}> $local
* @var array<string, array<string,mixed>> $manager_subscriptions
* @var list<array{type:string, message:string}> $manager_notices
*/
$client_configured = Robotstxt_Manager_Core_Client::from_options()->is_configured();
@ -65,6 +71,12 @@ $compat_warnings = 0;
<div class="notice notice-success is-dismissible"><p><?php esc_html_e( 'Catalog refreshed.', 'robotstxt-manager' ); ?></p></div>
<?php endif; ?>
<?php foreach ( ( $manager_notices ?? array() ) as $manager_notice ) : ?>
<div class="notice notice-<?php echo esc_attr( is_string( $manager_notice['type'] ?? '' ) ? $manager_notice['type'] : 'warning' ); ?>">
<p><?php echo esc_html( is_string( $manager_notice['message'] ?? '' ) ? $manager_notice['message'] : '' ); ?></p>
</div>
<?php endforeach; ?>
<?php if ( '' !== $notice_message && in_array( $notice_result, array( 'success', 'error' ), true ) ) : ?>
<div class="notice notice-<?php echo esc_attr( $notice_result ); ?> is-dismissible"><p><?php echo esc_html( $notice_message ); ?></p></div>
<?php endif; ?>
@ -209,6 +221,46 @@ $compat_warnings = 0;
number_format_i18n( $cost, 0 )
)
);
// Subscription pill for premium plugins.
$sub_row = $manager_subscriptions[ $slug ] ?? null;
$sub_text = '';
if ( is_array( $sub_row ) ) {
$raw_sub_status = $sub_row['status'] ?? '';
$raw_sub_expiry = $sub_row['expires_at'] ?? '';
$sub_status = is_string( $raw_sub_status ) ? $raw_sub_status : '';
$sub_expiry = is_string( $raw_sub_expiry ) ? $raw_sub_expiry : '';
if ( 'active' === $sub_status && '' !== $sub_expiry ) {
$days_left = (int) floor( ( (int) strtotime( $sub_expiry ) - time() ) / DAY_IN_SECONDS );
if ( $days_left >= 0 && $days_left <= 14 ) {
$sub_text = sprintf(
/* translators: %d: days remaining. */
__( 'Subscribed — %d days left', 'robotstxt-manager' ),
$days_left
);
} else {
$sub_text = __( 'Subscribed', 'robotstxt-manager' );
}
} elseif ( 'payment_failed' === $sub_status ) {
$sub_text = __( 'Payment failed', 'robotstxt-manager' );
} elseif ( 'cancelled' === $sub_status ) {
$sub_text = __( 'Cancelled', 'robotstxt-manager' );
} elseif ( 'expired' === $sub_status ) {
$sub_text = __( 'Expired', 'robotstxt-manager' );
}
}
if ( '' !== $sub_text ) {
$raw_pill_status = is_array( $sub_row ) ? ( $sub_row['status'] ?? '' ) : '';
$pill_status = is_string( $raw_pill_status ) ? $raw_pill_status : '';
echo '<br /><span class="robotstxt-manager-subscription robotstxt-manager-subscription--'
. esc_attr( $pill_status )
. '">' . esc_html( $sub_text ) . '</span>';
}
} else {
echo esc_html__( 'Free', 'robotstxt-manager' );
}

View file

@ -1,5 +1,22 @@
== Changelog ==
= 1.2.0 =
_Release date: 2026-08-15_
**Highlights**
* Subscription status, visible at a glance (Phase 4). The catalog now pulls the account's subscriptions from Core (`GET /me/subscriptions`, cached for one hour; refreshed on catalog refresh and key change) and shows a pill next to each premium plugin's price: Subscribed (with "N days left" when expiring within 14 days), Payment failed, Cancelled, or Expired. Admin notices at the top of the page warn about failed payments, subscriptions expiring within 14 days, and expired subscriptions whose plugin is still active on this site.
**Changed**
* Plugin version 1.1.0 → 1.2.0. No database schema changes (no custom tables).
**Compatibility**
* WordPress: 4.4 - 7.1 (scan-verified: wp-compat clean from 4.4)
* PHP: 8.0 - 8.5 (scan-verified: PHPCompatibility + manual feature audit)
= 1.1.0 =
_Release date: 2026-08-15_

View file

@ -280,4 +280,77 @@ class Robotstxt_Manager_Core_Client {
)
);
}
/**
* Returns the account's subscriptions (Core's /me/subscriptions),
* cached in a transient for one hour. Keys: plugin_slug => row.
*
* @return array<string, array<string, mixed>> Rows keyed by plugin slug.
*/
public function get_subscriptions(): array {
if ( ! $this->is_configured() ) {
return array();
}
$cache_key = 'robotstxt_manager_subscriptions';
$cached = get_transient( $cache_key );
if ( is_array( $cached ) ) {
$typed = array();
foreach ( $cached as $slug => $row ) {
if ( is_string( $slug ) && is_array( $row ) ) {
$typed[ $slug ] = $row;
}
}
return $typed;
}
$response = $this->get( '/me/subscriptions' );
if ( is_wp_error( $response ) || 200 !== (int) wp_remote_retrieve_response_code( $response ) ) {
return array(); // Not cached — retried on the next view.
}
$data = json_decode( wp_remote_retrieve_body( $response ), true );
if ( ! is_array( $data ) ) {
return array();
}
$rows = array();
foreach ( $data as $row ) {
if ( ! is_array( $row ) ) {
continue;
}
$slug = $row['plugin_slug'] ?? '';
$slug = is_string( $slug ) ? $slug : '';
if ( '' === $slug ) {
continue;
}
$raw_status = $row['status'] ?? '';
$raw_expires_at = $row['expires_at'] ?? '';
$rows[ $slug ] = array(
'status' => is_string( $raw_status ) ? $raw_status : '',
'expires_at' => is_string( $raw_expires_at ) ? $raw_expires_at : '',
);
}
set_transient( $cache_key, $rows, HOUR_IN_SECONDS );
return $rows;
}
/**
* Clears the cached subscriptions response (key change, manual refresh).
*
* @return void
*/
public function clear_subscriptions_cache(): void {
delete_transient( 'robotstxt_manager_subscriptions' );
}
}

Binary file not shown.

View file

@ -267,3 +267,29 @@ msgstr "El plugin baixat per a %s no té lestructura de carpetes esperada."
msgid "Requires: %s"
msgstr "Requereix: %s"
msgid "The payment for %s failed. Update your payment method from your ROBOTSTXT account page to keep access."
msgstr "El pagament de %s ha fallat. Actualitza el teu mètode de pagament des de la teva pàgina de compte de ROBOTSTXT per mantenir l'accés."
msgid "The subscription for %s has expired, but the plugin is still active on this site. Renew from your ROBOTSTXT account page to keep receiving updates."
msgstr "La subscripció de %s ha caducat, però el plugin encara és actiu en aquest lloc. Renova-la des de la teva pàgina de compte de ROBOTSTXT per seguir rebent actualitzacions."
msgid "Subscribed"
msgstr "Subscrit"
msgid "Subscribed — %d days left"
msgstr "Subscrit — queden %d dies"
msgid "Payment failed"
msgstr "Pagament fallit"
msgid "Cancelled"
msgstr "Cancel·lada"
msgid "Expired"
msgstr "Caducada"
msgid "The subscription for %1$s expires in %2$d day."
msgid_plural "The subscription for %1$s expires in %2$d days."
msgstr[0] "La subscripció de %1$s caduca en %2$d dia."
msgstr[1] "La subscripció de %1$s caduca en %2$d dies."

Binary file not shown.

View file

@ -267,3 +267,29 @@ msgstr "El plugin descargado para %s no tiene la estructura de carpetas esperada
msgid "Requires: %s"
msgstr "Requiere: %s"
msgid "The payment for %s failed. Update your payment method from your ROBOTSTXT account page to keep access."
msgstr "El pago de %s falló. Actualiza tu método de pago desde tu página de cuenta de ROBOTSTXT para mantener el acceso."
msgid "The subscription for %s has expired, but the plugin is still active on this site. Renew from your ROBOTSTXT account page to keep receiving updates."
msgstr "La suscripción de %s ha caducado, pero el plugin sigue activo en este sitio. Renuévela desde tu página de cuenta de ROBOTSTXT para seguir recibiendo actualizaciones."
msgid "Subscribed"
msgstr "Suscrito"
msgid "Subscribed — %d days left"
msgstr "Suscrito — quedan %d días"
msgid "Payment failed"
msgstr "Pago fallido"
msgid "Cancelled"
msgstr "Cancelada"
msgid "Expired"
msgstr "Caducada"
msgid "The subscription for %1$s expires in %2$d day."
msgid_plural "The subscription for %1$s expires in %2$d days."
msgstr[0] "La suscripción de %1$s caduca en %2$d día."
msgstr[1] "La suscripción de %1$s caduca en %2$d días."

View file

@ -1,268 +1,27 @@
# Copyright (C) 2026 ROBOTSTXT
# This file is distributed under the GPL-3.0-or-later.
msgid ""
msgstr ""
"Project-Id-Version: Manager (by ROBOTSTXT) 0.6.0\n"
"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/robotstxt-manager\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"POT-Creation-Date: 2026-08-15T12:00:00+00:00\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"X-Generator: manual\n"
"X-Domain: robotstxt-manager\n"
msgid "%d plugin in the catalog is not compatible with this site's WordPress or PHP version."
msgstr ""
msgid "%s activated."
msgstr ""
msgid "%s installed. Activate it from the list below."
msgstr ""
msgid "%s updated."
msgstr ""
msgid "(about %s)"
msgstr ""
msgid "A key is stored (last 4 characters: <code>%s</code>). Leave blank to keep the existing key; enter a new value to replace it."
msgstr ""
msgid "A key is stored but could not be decoded. You can replace it by entering a new value above, or delete it with the button below."
msgstr ""
msgid "API Key"
msgstr ""
msgid "API key deleted. Save changes to persist."
msgstr ""
msgid "API key is not configured."
msgstr ""
msgid "Account-level API key issued by the ROBOTSTXT store. Required to authenticate catalog and subscription requests. The value is encrypted before storage."
msgstr ""
msgid "Action"
msgstr ""
msgid "Activate"
msgstr ""
msgid "An unexpected error occurred."
msgstr ""
msgid "Base URL of the remote Plugins Core installation that this site will pull the plugin catalog from."
msgstr ""
msgid "Buy"
msgstr ""
msgid "Cache"
msgstr ""
msgid "Catalog Cache (minutes)"
msgstr ""
msgid "Catalog refreshed."
msgstr ""
msgid "Connected."
msgstr ""
msgid "Connection"
msgstr ""
msgid "Could not create a temporary file for download."
msgstr ""
msgid "Could not reach Plugins Core: %s"
msgstr ""
msgid "Data on Uninstall"
msgstr ""
msgid "Delete API key"
msgstr ""
msgid "Delete all plugin data when the plugin is uninstalled."
msgstr ""
msgid "Delete the stored API key? The catalog and subscription data will stop working until a new key is entered."
msgstr ""
msgid "Deleting…"
msgid "The payment for %s failed. Update your payment method from your ROBOTSTXT account page to keep access."
msgstr ""
msgid "Download failed (HTTP %d)."
msgid "The subscription for %s has expired, but the plugin is still active on this site. Renew from your ROBOTSTXT account page to keep receiving updates."
msgstr ""
msgid "Download failed: %s"
msgstr ""
msgid "Free"
msgstr ""
msgid "How long the catalog response from Core is cached in a transient. Default: 60 minutes. Lower values refresh more often at the cost of more requests to Core. Maximum: 1440 (24 hours)."
msgstr ""
msgid "Incompatible"
msgstr ""
msgid "Install"
msgstr ""
msgid "Installation failed."
msgstr ""
msgid "Installation failed: %s"
msgstr ""
msgid "Installed (inactive)"
msgstr ""
msgid "Insufficient permissions."
msgstr ""
msgid "Manager (by ROBOTSTXT) — Plugins"
msgstr ""
msgid "Manager (by ROBOTSTXT) — Settings"
msgstr ""
msgid "No plugins were returned by the ROBOTSTXT store. Check the Store URL and API key on the Settings page, or click Refresh to try again."
msgstr ""
msgid "Not installed"
msgstr ""
msgid "Plugin"
msgstr ""
msgid "Plugin is not installed."
msgstr ""
msgid "Plugin not found in catalog."
msgstr ""
msgid "Price"
msgstr ""
msgid "ROBOTSTXT"
msgstr ""
msgid "ROBOTSTXT Manager is not configured yet. <a href=\"%s\">Set the Store URL and API key</a> to see your plugin catalog."
msgstr ""
msgid "Refresh catalog"
msgstr ""
msgid "Requires PHP"
msgstr ""
msgid "Requires WP"
msgstr ""
msgid "Settings"
msgstr ""
msgid "Store URL"
msgstr ""
msgid "Store URL is not configured."
msgstr ""
msgid "Store is not configured."
msgstr ""
msgid "Test connection"
msgstr ""
msgid "Testing…"
msgstr ""
msgid "The API key format is invalid. Copy the full key from your ROBOTSTXT account page."
msgstr ""
msgid "The store responded with HTTP %d. Check the Store URL and API key."
msgstr ""
msgid "The store returned an invalid file."
msgstr ""
msgid "This site runs WordPress %1$s on PHP %2$s."
msgstr ""
msgid "Too many refreshes. Please wait a minute before refreshing again."
msgstr ""
msgid "Up to date"
msgstr ""
msgid "Update"
msgstr ""
msgid "Update available (v%1$s → v%2$s)"
msgstr ""
msgid "Visit website"
msgstr ""
msgid "You do not have sufficient permissions to access this page."
msgstr ""
msgid "Your server runs PHP"
msgstr ""
msgid "Your site runs WordPress"
msgstr ""
msgid "This is the ROBOTSTXT plugin store: the catalog of plugins published by ROBOTSTXT, installable and updatable straight from your own wp-admin. Free plugins install with one click; premium plugins require an annual subscription that you purchase on our website."
msgstr ""
msgid "Version"
msgstr ""
msgid "Status"
msgstr ""
msgid "€%s / year"
msgstr ""
msgid "Support"
msgstr ""
msgid "Need help with a ROBOTSTXT plugin? Visit the plugin's website (the link in its row above) for documentation and guides, or contact us through our website — we are happy to help."
msgstr ""
msgid "Payments, and how it works"
msgstr ""
msgid "Free plugins install instantly at no cost. Premium plugins are annual subscriptions: you pay once on our website and the subscription renews automatically every year until you cancel. You can cancel at any time from your ROBOTSTXT account page — access keeps working until the end of the paid period. All payments are processed securely by Mollie; we never see or store your card details."
msgstr ""
msgid "Installed required plugins: %s."
msgstr ""
msgid "The subscription for %1$s expires in %2$d day."
msgid_plural "The subscription for %1$s expires in %2$d days."
msgstr[0] ""
msgstr[1] ""
msgid "Could not install the required plugin %s."
msgid "Subscribed"
msgstr ""
msgid "WordPress.org lookup failed: %s"
msgid "Subscribed — %d days left"
msgstr ""
msgid "No download found on WordPress.org for %s."
msgid "Payment failed"
msgstr ""
msgid "The downloaded plugin for %s does not have the expected folder structure."
msgid "Cancelled"
msgstr ""
msgid "Requires: %s"
msgid "Expired"
msgstr ""

View file

@ -3,9 +3,9 @@ Contributors: javiercasares, robotstxt
Tags: dashboard, catalog, updates, subscriptions, management
Requires at least: 4.4
Tested up to: 7.1
Stable tag: 1.1.0
Stable tag: 1.2.0
Requires PHP: 8.0
Version: 1.1.0
Version: 1.2.0
License: GPL-3.0-or-later
License URI: https://www.gnu.org/licenses/gpl-3.0.txt
@ -92,6 +92,12 @@ Encrypted at rest using AES-256-CBC with a key derived from your site's WordPres
== Changelog ==
= 1.2.0 =
_Release date: 2026-08-15_
* Subscription status (Phase 4): pills next to premium prices (Subscribed / N days left / Payment failed / Cancelled / Expired) and admin notices for failed payments, upcoming expiries, and expired-but-active plugins. Cached hourly from Core's /me/subscriptions.
= 1.1.0 =
_Release date: 2026-08-15_

View file

@ -3,7 +3,7 @@
* Plugin Name: Manager (by ROBOTSTXT)
* Plugin URI: https://git.robotstxt.es/ROBOTSTXT/robotstxt-manager
* Description: Client-side dashboard for the ROBOTSTXT plugin ecosystem. Lists the catalog from a remote Plugins Core install, resolves local install/update state, and installs, activates, and updates plugins directly from the store.
* Version: 1.1.0
* Version: 1.2.0
* Requires at least: 4.4
* Requires PHP: 8.0
* Author: ROBOTSTXT
@ -21,7 +21,7 @@ if ( ! defined( 'ABSPATH' ) ) {
}
/** Plugin version. */
define( 'ROBOTSTXT_MANAGER_VERSION', '1.1.0' );
define( 'ROBOTSTXT_MANAGER_VERSION', '1.2.0' );
/** Absolute path to the plugin directory, with trailing slash. */
define( 'ROBOTSTXT_MANAGER_DIR', plugin_dir_path( __FILE__ ) );