diff --git a/CHANGELOG.md b/CHANGELOG.md
index 20c9416b..7966e302 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -31,6 +31,7 @@
- **HQ config sync** — client stores HQ-configured `support_pin_hours` from heartbeat response, PIN TTL now configurable from HQ
### Changed
+- **Support PIN UI unified** — `SupportPinHelper::renderBadge()` and `renderScript()` replace 3 separate inline implementations (dashboard, cpanel module, cache module) with click-to-copy on all PIN badges
- Admin sidebar menu module now loads component-local language files (fixes untranslated keys for MokoSuiteCross and other components)
- Support PIN TTL is now configurable via HQ global options instead of hardcoded 72 hours
- Removed MokoSuiteHQ from extension catalog (internal app, not for client sites)
diff --git a/source/packages/com_mokosuiteclient/admin/src/Helper/SupportPinHelper.php b/source/packages/com_mokosuiteclient/admin/src/Helper/SupportPinHelper.php
index 3d1569e3..cb778d82 100644
--- a/source/packages/com_mokosuiteclient/admin/src/Helper/SupportPinHelper.php
+++ b/source/packages/com_mokosuiteclient/admin/src/Helper/SupportPinHelper.php
@@ -115,6 +115,145 @@ class SupportPinHelper
*
* @return array{success: bool, pin?: string, message: string}
*/
+ /**
+ * Render PIN badge HTML (active PIN with copy, or request button).
+ *
+ * @param array $state Result from getState().
+ * @param string $token CSRF form token name.
+ * @param string $context 'dashboard'|'cpanel'|'cache' — controls layout variant.
+ *
+ * @return string HTML fragment (no wrapping div).
+ */
+ public static function renderBadge(array $state, string $token, string $context = 'dashboard'): string
+ {
+ if (!$state['available'])
+ {
+ return '';
+ }
+
+ $requestUrl = \Joomla\CMS\Router\Route::_('index.php?option=com_mokosuiteclient&task=display.requestPin&format=json');
+ $pin = $state['pin'];
+
+ $html = '';
+
+ if (!empty($pin))
+ {
+ $escaped = htmlspecialchars($pin, ENT_QUOTES, 'UTF-8');
+
+ if ($context === 'cache')
+ {
+ $html .= '';
+ $html .= '';
+ $html .= '' . $escaped . '';
+ $html .= '';
+ }
+ else
+ {
+ $html .= '';
+ $html .= '' . $escaped . '';
+ }
+ }
+ else
+ {
+ if ($context === 'cache')
+ {
+ $html .= '';
+ $html .= '';
+ $html .= 'PIN';
+ $html .= '';
+ }
+ else
+ {
+ $html .= '';
+ }
+ }
+
+ return $html;
+ }
+
+ /**
+ * Render shared JS for PIN copy and request functionality.
+ *
+ * @return string
+JS;
+ }
+
public static function requestNew(DatabaseInterface $db): array
{
$state = self::getState($db);
diff --git a/source/packages/com_mokosuiteclient/admin/tmpl/dashboard/default.php b/source/packages/com_mokosuiteclient/admin/tmpl/dashboard/default.php
index a9d19bea..ca33d065 100644
--- a/source/packages/com_mokosuiteclient/admin/tmpl/dashboard/default.php
+++ b/source/packages/com_mokosuiteclient/admin/tmpl/dashboard/default.php
@@ -56,21 +56,17 @@ $actionLogsEnabled = Joomla\CMS\Component\ComponentHelper::isEnabled('com_action
escape($siteInfo->sitename); ?>
MokoSuite escape($siteInfo->mokosuiteclient_version); ?>
+ !empty($this->supportPinAvailable), 'pin' => $this->supportPin ?? ''],
+ $token, 'dashboard'
+ ); ?>
supportPin)): ?>
- escape($this->supportPin); ?>
- supportPinAvailable)): ?>
-
Joomla escape($siteInfo->joomla_version); ?>
PHP escape($siteInfo->php_version); ?>
@@ -459,3 +455,5 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
+
+
diff --git a/source/packages/com_mokosuiteclient/media/js/dashboard.js b/source/packages/com_mokosuiteclient/media/js/dashboard.js
index cbf39e9d..88ae33c0 100644
--- a/source/packages/com_mokosuiteclient/media/js/dashboard.js
+++ b/source/packages/com_mokosuiteclient/media/js/dashboard.js
@@ -144,43 +144,6 @@ document.addEventListener('DOMContentLoaded', function () {
});
}
- // Request PIN button
- var pinBtn = document.getElementById('mokosuiteclient-request-pin');
- if (pinBtn) {
- pinBtn.addEventListener('click', function () {
- var btn = this;
- btn.disabled = true;
- btn.textContent = '...';
- var fd = new FormData();
- fd.append(btn.dataset.token, '1');
- fetch(btn.dataset.url, {method: 'POST', body: fd, headers: {'X-Requested-With': 'XMLHttpRequest'}})
- .then(function (r) { return r.json(); })
- .then(function (d) {
- if (d.success && d.pin) {
- var badge = document.createElement('span');
- badge.className = 'badge bg-dark';
- badge.style.cssText = 'font-family:monospace;letter-spacing:0.08em;cursor:help;';
- badge.title = 'Support PIN — valid for 72 hours';
- badge.textContent = d.pin;
- var icon = document.createElement('span');
- icon.className = 'icon-key small me-1';
- icon.setAttribute('aria-hidden', 'true');
- badge.prepend(icon);
- btn.replaceWith(badge);
- } else {
- Joomla.renderMessages({error: [d.message || 'Failed to generate PIN']});
- btn.disabled = false;
- btn.textContent = 'Request PIN';
- }
- })
- .catch(function () {
- Joomla.renderMessages({error: ['Network error']});
- btn.disabled = false;
- btn.textContent = 'Request PIN';
- });
- });
- }
-
// Regular Labs import
var rlBtn = document.getElementById('btn-import-regularlabs');
if (rlBtn) {
diff --git a/source/packages/mod_mokosuiteclient_cache/tmpl/default.php b/source/packages/mod_mokosuiteclient_cache/tmpl/default.php
index 1d46d390..34c111e3 100644
--- a/source/packages/mod_mokosuiteclient_cache/tmpl/default.php
+++ b/source/packages/mod_mokosuiteclient_cache/tmpl/default.php
@@ -12,7 +12,6 @@ use Joomla\CMS\Session\Session;
$token = Session::getFormToken();
$cacheUrl = 'index.php?option=com_mokosuiteclient&task=display.clearCache&format=json';
$tempUrl = 'index.php?option=com_mokosuiteclient&task=display.clearTemp&format=json';
-$pinUrl = 'index.php?option=com_mokosuiteclient&task=display.requestPin&format=json';
$pinAvailable = $supportPinAvailable ?? false;
$pin = $supportPin ?? '';
$frontendUrl = $frontendUrl ?? '';
@@ -25,12 +24,16 @@ $frontendUrl = $frontendUrl ?? '';
Site
-
-
-
-
-
-
+ true, 'pin' => $pin],
+ $token, 'cache'
+ );
+ if (!$frontendUrl) {
+ $pinHtml = str_replace('rounded-0 border-end-0', 'rounded-0 rounded-start border-end-0', $pinHtml);
+ }
+ echo $pinHtml;
+ endif; ?>
Cache
@@ -92,55 +95,6 @@ document.addEventListener('DOMContentLoaded', function() {
setupCleaner('mokosuiteclient-clear-cache', 'mokosuiteclient-cache-icon', '', '');
setupCleaner('mokosuiteclient-clear-temp', 'mokosuiteclient-temp-icon', '', '');
- // Support PIN button
- var pinBtn = document.getElementById('mokosuiteclient-pin');
- var pinIcon = document.getElementById('mokosuiteclient-pin-icon');
- var pinText = document.getElementById('mokosuiteclient-pin-text');
- if (pinBtn && pinText) {
- pinBtn.addEventListener('click', function(e) {
- e.preventDefault();
- var current = pinText.textContent.trim();
-
- if (current.indexOf('MOKO-') === 0) {
- navigator.clipboard.writeText(current).then(function() {
- var orig = pinText.textContent;
- pinText.textContent = 'Copied!';
- setTimeout(function() { pinText.textContent = orig; }, 1500);
- });
- return;
- }
-
- if (pinBtn.dataset.busy) return;
- pinBtn.dataset.busy = '1';
- if (pinIcon) pinIcon.className = 'icon-spinner icon-spin';
- pinText.textContent = '...';
-
- var fd = new FormData();
- fd.append('', '1');
-
- fetch('', {
- method: 'POST',
- headers: {'X-Requested-With': 'XMLHttpRequest'},
- body: fd
- })
- .then(function(r) { return r.json(); })
- .then(function(data) {
- if (data.success && data.pin) {
- pinText.textContent = data.pin;
- pinBtn.title = 'Support PIN — click to copy';
- if (pinIcon) pinIcon.className = 'icon-key';
- } else {
- pinText.textContent = 'PIN';
- if (pinIcon) pinIcon.className = 'icon-key';
- }
- delete pinBtn.dataset.busy;
- })
- .catch(function() {
- pinText.textContent = 'PIN';
- if (pinIcon) pinIcon.className = 'icon-key';
- delete pinBtn.dataset.busy;
- });
- });
- }
});
+
diff --git a/source/packages/mod_mokosuiteclient_cpanel/tmpl/default.php b/source/packages/mod_mokosuiteclient_cpanel/tmpl/default.php
index 4263c6ea..9a3fbc97 100644
--- a/source/packages/mod_mokosuiteclient_cpanel/tmpl/default.php
+++ b/source/packages/mod_mokosuiteclient_cpanel/tmpl/default.php
@@ -70,16 +70,10 @@ $diskColor = ($diskPct !== null && $diskPct > 90) ? 'bg-danger' : (($diskPct !==
sitename ?? ''); ?>
MokoSuite mokosuiteclient_version ?? ''); ?>
-
-
-
-
-
+ !empty($supportPinAvailable), 'pin' => $supportPin ?? ''],
+ $token, 'cpanel'
+ ); ?>
Joomla joomla_version ?? ''); ?>
PHP php_version ?? ''); ?>
db_type ?? ''); ?>
@@ -95,37 +89,4 @@ $diskColor = ($diskPct !== null && $diskPct > 90) ? 'bg-danger' : (($diskPct !==
-
+