diff --git a/.upgradenotes/MDL-87494-2026021908424434.yml b/.upgradenotes/MDL-87494-2026021908424434.yml
new file mode 100644
index 00000000000..896006b1c11
--- /dev/null
+++ b/.upgradenotes/MDL-87494-2026021908424434.yml
@@ -0,0 +1,11 @@
+issueNumber: MDL-87494
+notes:
+ tool_mobile:
+ - message: >-
+ Improve the mobile app subscription page UI and add a subscription
+ cache refresh task and an application-level cache.
+ The cache name used for mobile subscription information has changed,
+ the get_subscription() helper now accepts additional parameters
+ and the undocumented config.php setting
+ $CFG->disablemobileappsubscription has been removed.
+ type: changed
diff --git a/public/admin/tool/mobile/amd/build/featurecomparison.min.js b/public/admin/tool/mobile/amd/build/featurecomparison.min.js
new file mode 100644
index 00000000000..b3cb9a0c811
--- /dev/null
+++ b/public/admin/tool/mobile/amd/build/featurecomparison.min.js
@@ -0,0 +1,9 @@
+define("tool_mobile/featurecomparison",["exports","core/str","core/notification"],(function(_exports,_str,_notification){var obj;
+/**
+ * Feature comparison toggle for tool_mobile subscription page.
+ *
+ * @copyright 2026 Daniel Urena
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_notification=(obj=_notification)&&obj.__esModule?obj:{default:obj};const initCard=card=>{if(!card)return;const rows=Array.from(card.querySelectorAll(".feature-comparison-rows .feature-comparison-row")),toggle=card.querySelector(".feature-comparison-toggle");if(!rows.length||!toggle)return;const textSpan=toggle.querySelector(".feature-comparison-toggle-text"),icon=toggle.querySelector(".feature-comparison-toggle-icon");let expanded=!1;const applyState=()=>(rows.forEach((row=>{row.classList.toggle("d-none",!expanded)})),textSpan&&(0,_str.getString)(expanded?"showless":"showmore").then((str=>(textSpan.textContent=str,null))).catch((error=>{_notification.default.exception(error)})),icon&&(icon.classList.toggle("fa-chevron-down",!expanded),icon.classList.toggle("fa-chevron-up",expanded)),null);toggle.addEventListener("click",(e=>{e.preventDefault(),expanded=!expanded,applyState()})),applyState()};var _default={init:()=>{document.querySelectorAll(".feature-comparison-card").forEach(initCard)}};return _exports.default=_default,_exports.default}));
+
+//# sourceMappingURL=featurecomparison.min.js.map
\ No newline at end of file
diff --git a/public/admin/tool/mobile/amd/build/featurecomparison.min.js.map b/public/admin/tool/mobile/amd/build/featurecomparison.min.js.map
new file mode 100644
index 00000000000..5817ddd7bf7
--- /dev/null
+++ b/public/admin/tool/mobile/amd/build/featurecomparison.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"featurecomparison.min.js","sources":["../src/featurecomparison.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Feature comparison toggle for tool_mobile subscription page.\n *\n * @copyright 2026 Daniel Urena\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {getString} from 'core/str';\nimport Notification from 'core/notification';\n\n/**\n * Initialise a single feature comparison card.\n *\n * @param {HTMLElement} card\n */\nconst initCard = (card) => {\n if (!card) {\n return;\n }\n\n const rows = Array.from(\n card.querySelectorAll('.feature-comparison-rows .feature-comparison-row')\n );\n const toggle = card.querySelector('.feature-comparison-toggle');\n\n if (!rows.length || !toggle) {\n return;\n }\n\n const textSpan = toggle.querySelector('.feature-comparison-toggle-text');\n const icon = toggle.querySelector('.feature-comparison-toggle-icon');\n\n let expanded = false;\n\n const applyState = () => {\n rows.forEach((row) => {\n row.classList.toggle('d-none', !expanded);\n });\n\n if (textSpan) {\n // Fire and forget: handle resolution/rejection explicitly.\n void getString(expanded ? 'showless' : 'showmore')\n .then((str) => {\n textSpan.textContent = str;\n return null;\n })\n .catch((error) => {\n Notification.exception(error);\n });\n }\n\n if (icon) {\n icon.classList.toggle('fa-chevron-down', !expanded);\n icon.classList.toggle('fa-chevron-up', expanded);\n }\n\n return null;\n };\n\n toggle.addEventListener('click', (e) => {\n e.preventDefault();\n expanded = !expanded;\n applyState();\n });\n\n applyState();\n};\n\n/**\n * Initialise all feature comparison cards.\n */\nconst init = () => {\n document.querySelectorAll('.feature-comparison-card')\n .forEach(initCard);\n};\n\nexport default {\n init,\n};\n"],"names":["initCard","card","rows","Array","from","querySelectorAll","toggle","querySelector","length","textSpan","icon","expanded","applyState","forEach","row","classList","then","str","textContent","catch","error","exception","addEventListener","e","preventDefault","init","document"],"mappings":";;;;;;+JA8BMA,SAAYC,WACTA,kBAICC,KAAOC,MAAMC,KACfH,KAAKI,iBAAiB,qDAEpBC,OAASL,KAAKM,cAAc,kCAE7BL,KAAKM,SAAWF,oBAIfG,SAAWH,OAAOC,cAAc,mCAChCG,KAAOJ,OAAOC,cAAc,uCAE9BI,UAAW,QAETC,WAAa,KACfV,KAAKW,SAASC,MACVA,IAAIC,UAAUT,OAAO,UAAWK,aAGhCF,WAEK,kBAAUE,SAAW,WAAa,YAClCK,MAAMC,MACHR,SAASS,YAAcD,IAChB,QAEVE,OAAOC,8BACSC,UAAUD,UAI/BV,OACAA,KAAKK,UAAUT,OAAO,mBAAoBK,UAC1CD,KAAKK,UAAUT,OAAO,gBAAiBK,WAGpC,MAGXL,OAAOgB,iBAAiB,SAAUC,IAC9BA,EAAEC,iBACFb,UAAYA,SACZC,gBAGJA,2BAWW,CACXa,KANS,KACTC,SAASrB,iBAAiB,4BACrBQ,QAAQb"}
\ No newline at end of file
diff --git a/public/admin/tool/mobile/amd/src/featurecomparison.js b/public/admin/tool/mobile/amd/src/featurecomparison.js
new file mode 100644
index 00000000000..3225098d27b
--- /dev/null
+++ b/public/admin/tool/mobile/amd/src/featurecomparison.js
@@ -0,0 +1,94 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see .
+
+/**
+ * Feature comparison toggle for tool_mobile subscription page.
+ *
+ * @copyright 2026 Daniel Urena
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import {getString} from 'core/str';
+import Notification from 'core/notification';
+
+/**
+ * Initialise a single feature comparison card.
+ *
+ * @param {HTMLElement} card
+ */
+const initCard = (card) => {
+ if (!card) {
+ return;
+ }
+
+ const rows = Array.from(
+ card.querySelectorAll('.feature-comparison-rows .feature-comparison-row')
+ );
+ const toggle = card.querySelector('.feature-comparison-toggle');
+
+ if (!rows.length || !toggle) {
+ return;
+ }
+
+ const textSpan = toggle.querySelector('.feature-comparison-toggle-text');
+ const icon = toggle.querySelector('.feature-comparison-toggle-icon');
+
+ let expanded = false;
+
+ const applyState = () => {
+ rows.forEach((row) => {
+ row.classList.toggle('d-none', !expanded);
+ });
+
+ if (textSpan) {
+ // Fire and forget: handle resolution/rejection explicitly.
+ void getString(expanded ? 'showless' : 'showmore')
+ .then((str) => {
+ textSpan.textContent = str;
+ return null;
+ })
+ .catch((error) => {
+ Notification.exception(error);
+ });
+ }
+
+ if (icon) {
+ icon.classList.toggle('fa-chevron-down', !expanded);
+ icon.classList.toggle('fa-chevron-up', expanded);
+ }
+
+ return null;
+ };
+
+ toggle.addEventListener('click', (e) => {
+ e.preventDefault();
+ expanded = !expanded;
+ applyState();
+ });
+
+ applyState();
+};
+
+/**
+ * Initialise all feature comparison cards.
+ */
+const init = () => {
+ document.querySelectorAll('.feature-comparison-card')
+ .forEach(initCard);
+};
+
+export default {
+ init,
+};
diff --git a/public/admin/tool/mobile/classes/api.php b/public/admin/tool/mobile/classes/api.php
index 5f380d67251..eaec29b2814 100644
--- a/public/admin/tool/mobile/classes/api.php
+++ b/public/admin/tool/mobile/classes/api.php
@@ -802,15 +802,26 @@ class api {
/**
* Gets Moodle app plan subscription information for the current site as it is returned by the Apps Portal.
*
+ * @param bool $forcecache If true, return only cached data. Has priority over $ignorecache.
+ * @param bool $ignorecache If true, ignore cached data and request information from the Application Portal.
+ * @param int $timeout Time in seconds to wait for the Apps Portal response before giving up. Defaults to 10 seconds.
* @return array Subscription information
*/
- public static function get_subscription_information(): ?array {
+ public static function get_subscription_information($forcecache = false, $ignorecache = false, $timeout = 10): ?array {
global $CFG;
- // Use session cache to prevent multiple requests.
- $cache = \cache::make('tool_mobile', 'subscriptiondata');
+ $timeout = min(30, $timeout);
+ // Manage cache of the subscription information to avoid requesting it too often to the Moodle Apps Portal.
+ $cache = \cache::make('tool_mobile', 'subscriptioninfo');
$subscriptiondata = $cache->get(0);
- if ($subscriptiondata !== false) {
+
+ // If we must force using cache, return it (or null if not present) and never contact the portal.
+ if ($forcecache) {
+ return $subscriptiondata !== false ? $subscriptiondata : null;
+ }
+
+ // If we are allowed to use cache and we have data, return it early.
+ if (!$ignorecache && $subscriptiondata !== false) {
return $subscriptiondata;
}
@@ -840,20 +851,25 @@ class api {
'appids' => [$androidappid, $iosappid],
'credentials' => $credentials,
];
+ // Get the current language to send to the WS.
+ $settingslang = current_language();
// Prepare the arguments for a request to the AJAX nologin endpoint.
$args = [
(object) [
'index' => 0,
'methodname' => 'local_apps_get_site_info',
'args' => $fnparams,
- ]
+ ],
];
// Ask the Moodle Apps Portal for the subscription information.
$curl = new curl();
- $curl->setopt(array('CURLOPT_TIMEOUT' => 10, 'CURLOPT_CONNECTTIMEOUT' => 10));
+ $curl->setopt([
+ 'CURLOPT_TIMEOUT' => $timeout,
+ 'CURLOPT_CONNECTTIMEOUT' => $timeout,
+ ]);
- $serverurl = static::MOODLE_APPS_PORTAL_URL . "/lib/ajax/service-nologin.php";
+ $serverurl = static::MOODLE_APPS_PORTAL_URL . "/lib/ajax/service-nologin.php?lang=$settingslang";
$query = 'args=' . urlencode(json_encode($args));
$wsresponse = @json_decode($curl->post($serverurl, $query), true);
@@ -861,10 +877,23 @@ class api {
if ($curlerrno = $curl->get_errno()) {
// CURL connection error.
debugging("Unexpected response from the Moodle Apps Portal server, CURL error number: $curlerrno");
+ if (!$ignorecache && $subscriptiondata !== false) {
+ return $subscriptiondata;
+ }
+ return null;
+ } else if (!empty($curl->error)) {
+ // CURL error without an error number.
+ debugging('Unexpected response from the Moodle Apps Portal server, CURL error: ' . $curl->error);
+ if (!$ignorecache && $subscriptiondata !== false) {
+ return $subscriptiondata;
+ }
return null;
} else if ($info['http_code'] != 200) {
// Unexpected error from server.
- debugging('Unexpected response from the Moodle Apps Portal server, HTTP code:' . $info['httpcode']);
+ debugging('Unexpected response from the Moodle Apps Portal server, HTTP code:' . $info['http_code']);
+ if (!$ignorecache && $subscriptiondata !== false) {
+ return $subscriptiondata;
+ }
return null;
} else if (!empty($wsresponse[0]['error'])) {
// Unexpected error from Moodle Apps Portal.
@@ -876,7 +905,7 @@ class api {
}
$cache->set(0, $wsresponse[0]['data']);
-
+ set_config('subscriptioninfoupdated', time(), 'tool_mobile');
return $wsresponse[0]['data'];
}
}
diff --git a/public/admin/tool/mobile/classes/external.php b/public/admin/tool/mobile/classes/external.php
index 83bb44c4302..5d80a52cc94 100644
--- a/public/admin/tool/mobile/classes/external.php
+++ b/public/admin/tool/mobile/classes/external.php
@@ -738,7 +738,7 @@ class external extends external_api {
$validated = false;
$sitesubscriptionkey = get_config('tool_mobile', 'sitesubscriptionkey');
- if (!empty($sitesubscriptionkey) && $CFG->enablemobilewebservice && empty($CFG->disablemobileappsubscription)) {
+ if (!empty($sitesubscriptionkey) && $CFG->enablemobilewebservice) {
$sitesubscriptionkey = json_decode($sitesubscriptionkey);
$validated = time() < $sitesubscriptionkey->validuntil && $params['key'] === $sitesubscriptionkey->key;
// Delete existing, even if not validated to enforce security and attacks prevention.
diff --git a/public/admin/tool/mobile/classes/output/subscription.php b/public/admin/tool/mobile/classes/output/subscription.php
index cf31dc3c2e5..c0c94c5b44d 100644
--- a/public/admin/tool/mobile/classes/output/subscription.php
+++ b/public/admin/tool/mobile/classes/output/subscription.php
@@ -59,59 +59,190 @@ class subscription implements \renderable, \templatable {
* @return array with the subscription information
*/
public function export_for_template(\renderer_base $output): array {
- global $CFG;
+ global $CFG, $USER;
$ms = get_config('tool_mobile'); // Get mobile settings.
$data = $this->subscriptiondata;
+ // Check subscription information to prepare the page data.
+ if (empty($data) || empty($data['subscription']) || !isset($data['availableplans'])) {
+ // Prepare data for no subscription information case, with a message and a link to the apps portal.
+ $datanosub['appsportalurl'] = \tool_mobile\api::MOODLE_APPS_PORTAL_URL;
+ $datanosub['messagenosubscriptioninfo'][] = [
+ 'message' => get_string('nosubsblocked', 'tool_mobile', $datanosub['appsportalurl']),
+ 'title' => get_string('nosubswhyhappen', 'tool_mobile'),
+ 'extraclasses' => 'mt-4 mb-0',
+ ];
+ return $datanosub;
+ }
+
+ $livedataurl = \tool_mobile\api::MOODLE_APPS_PORTAL_URL;
+ $data['messagelivedata'] = get_string('showinglivedata', 'tool_mobile', $livedataurl);
+
+ $lastupdate = get_config('tool_mobile', 'subscriptioninfoupdated');
+ if (empty($lastupdate) || time() - $lastupdate > 10 * DAYSECS) {
+ if (empty($lastupdate)) {
+ $data['messageoldcache'] = get_string('planneverchecked', 'tool_mobile');
+ } else {
+ $diff = time() - $lastupdate;
+ $oldcachetime['since'] = (int) floor($diff / DAYSECS);
+ $oldcachetime['date'] = userdate($lastupdate, get_string('strftimedatetime', 'langconfig'));
+ $data['messageoldcache'] = get_string('showingcacheddata', 'tool_mobile', $oldcachetime);
+ }
+ }
+
$data['appsportalurl'] = \tool_mobile\api::MOODLE_APPS_PORTAL_URL;
+ $params = [
+ 'sitesecret' => md5(\core\hub\registration::get_secret()),
+ 'siteurl' => $CFG->wwwroot,
+ 'origin' => 'moodlelms',
+ ];
+ $unregisteredurl = new \moodle_url(
+ \tool_mobile\api::MOODLE_APPS_PORTAL_URL . '/local/apps/signup_site.php',
+ $params,
+ );
+
+ $params['plan'] = 'premium';
+ $params['email'] = md5($USER->email);
+ $appsportalupgradeurl = new \moodle_url(
+ \tool_mobile\api::MOODLE_APPS_PORTAL_URL . '/local/apps/signup_site.php',
+ $params,
+ );
+ $data['appsportalupgradeurl'] = $appsportalupgradeurl->out(false);
// First prepare messages that may come from the WS.
if (!empty($data['messages'])) {
foreach ($data['messages'] as $msg) {
- $data['messages' . $msg['type']][] = ['message' => $msg['message']];
+ switch ($msg['code'] ?? '') {
+ case "deprecatedplan":
+ $data['messagedeprecation'][] = [
+ 'message' => $msg['message'],
+ 'titleicon' => [
+ 'icon' => 'i/warning',
+ 'component' => 'core',
+ ],
+ 'title' => get_string('plancontinues', 'tool_mobile', $data['subscription']['name']),
+ ];
+ break;
+ case "missingcredentials":
+ $data['messagemissingcredentials'][] = [
+ 'message' => $msg['message'],
+ 'extraclasses' => 'mt-4',
+ ];
+ break;
+
+ case "unregisteredsite":
+ $data['messageunregisteredsite'][] = [
+ 'message' => $msg['message'],
+ 'titleicon' => [
+ 'icon' => 'i/warning',
+ 'component' => 'core',
+ ],
+ 'buttonstr' => get_string('register', 'tool_mobile'),
+ 'buttonurl' => $unregisteredurl->out(false),
+ 'extrabuttonclasses' => 'text-dark',
+ ];
+ break;
+
+ default:
+ $data['messages' . $msg['type']][] = ['message' => $msg['message']];
+ break;
+ }
}
}
unset($data['messages']);
- // Now prepare statistics information.
- if (isset($data['statistics']['notifications'])) {
- $data['notifications'] = $data['statistics']['notifications'];
- unset($data['statistics']['notifications']);
-
- // Find current month data.
- $data['notifications']['currentactivedevices'] = 0;
-
- if (isset($data['notifications']['monthly'][0])) {
- $currentmonth = $data['notifications']['monthly'][0];
- $data['notifications']['currentactivedevices'] = $currentmonth['activedevices'];
- if (!empty($currentmonth['limitreachedtime'])) {
- $data['notifications']['limitreachedtime'] = $currentmonth['limitreachedtime'];
- $data['notifications']['ignorednotificationswarning'] = [
- 'message' => get_string('notificationslimitreached', 'tool_mobile', $data['appsportalurl'])
- ];
- }
- }
+ // Derive plan flags for template logic-less checks.
+ if (!empty($data['subscription']['plan'])) {
+ $data['subscription']['isfree'] = ($data['subscription']['plan'] === 'free');
+ $data['subscription']['ispremium'] =
+ ($data['subscription']['plan'] === 'premium' || $data['subscription']['plan'] === 'bma');
}
+ if ($data['subscription']['ispremium']) {
+ $data['messagefullaccess'][] = [
+ 'titleicon' => [
+ 'icon' => 'i/checkedcircle',
+ 'component' => 'core',
+ ],
+ 'title' => get_string('fullaccessfeatures', 'tool_mobile', $data['subscription']['name']),
+ ];
+ }
+
+ // Review availableplans.
+ foreach ($data['availableplans'] as $plan) {
+ if ($plan['plan'] != 'premium') {
+ continue;
+ }
+ $data['tocompareplan'] = $plan;
+ }
+
+ // Now prepare statistics information.
+ if (!isset($data['messagemissingcredentials'])) {
+ if (isset($data['statistics']['notifications'])) {
+ $data['notifications'] = $data['statistics']['notifications'];
+ unset($data['statistics']['notifications']);
+
+ // Find current month data.
+ $data['notifications']['currentactivedevices'] = 0;
+
+ if (isset($data['notifications']['monthly'][0])) {
+ $currentmonth = $data['notifications']['monthly'][0];
+ $data['notifications']['currentactivedevices'] = $currentmonth['activedevices'];
+ if (!empty($currentmonth['limitreachedtime'])) {
+ $data['notifications']['limitreachedtime'] = $currentmonth['limitreachedtime'];
+ $data['notifications']['ignorednotificationswarning'] = [
+ 'message' => get_string('notificationslimitreached', 'tool_mobile'),
+ 'icon' => [
+ 'icon' => 'i/risk_dataloss',
+ 'component' => 'core',
+ ],
+ 'buttonstr' => get_string('upgradetosubscription', 'tool_mobile', $data['tocompareplan']['name']),
+ 'buttonurl' => $appsportalupgradeurl->out(false),
+ 'extraclasses' => 'alert-danger',
+ 'extrabuttonclasses' => 'bg-danger text-light',
+ ];
+ }
+ $monthformat = get_string('strftimemonth');
+ foreach ($data['notifications']['monthly'] as $key => $month) {
+ if (!empty($month['year']) && !empty($month['month'])) {
+ $timestamp = make_timestamp((int)$month['year'], (int)$month['month'], 1);
+ $data['notifications']['monthly'][$key]['monthstr'] = userdate($timestamp, $monthformat);
+ }
+ }
+ }
+ $data['messagesnotificationsseemore'] = get_string('notificationsseemore', 'tool_mobile', $data['appsportalurl']);
+ } else {
+ $urlmessagesetting = new \moodle_url('/admin/settings.php', ['section' => 'messagesettingairnotifier']);
+ $data['messagesnonotifications'][] = [
+ 'icon' => [
+ 'icon' => 'i/warning',
+ 'component' => 'core',
+ ],
+ 'message' => get_string('notificationsmissingwarning', 'tool_mobile', $urlmessagesetting),
+ 'extraclasses' => 'mt-4',
+ ];
+ }
+ }
+ $subscribedfeatures = [];
// Review features.
foreach ($data['subscription']['features'] as &$feature) {
-
// Check the type of features, if it is a limitation or functionality feature.
if (array_key_exists('limit', $feature)) {
-
if (empty($feature['limit'])) { // Unlimited, no need to calculate current values.
$feature['humanstatus'] = get_string('unlimited');
$feature['showbar'] = 0;
- continue;
+ $feature['limit'] = get_string('unlimited');
}
switch ($feature['name']) {
// Check active devices.
case 'pushnotificationsdevices':
+ $feature['status'] = 0;
if (isset($data['notifications']['currentactivedevices'])) {
$feature['status'] = $data['notifications']['currentactivedevices'];
}
+ $feature['showstatus'] = 1;
break;
// Check menu items.
case 'custommenuitems':
@@ -140,6 +271,7 @@ class subscription implements \renderable, \templatable {
}
$feature['status'] = count($custommenuitems) + count($customusermenuitems);
+ $feature['showstatus'] = 1;
break;
// Check language strings.
case 'customlanguagestrings':
@@ -155,29 +287,59 @@ class subscription implements \renderable, \templatable {
);
}
$feature['status'] = count($langstrings);
+ $feature['showstatus'] = 1;
break;
// Check disabled features strings.
case 'disabledfeatures':
$feature['status'] = empty($ms->disabledfeatures) ? 0 : count(explode(',', $ms->disabledfeatures));
+ $feature['showstatus'] = 1;
break;
}
$feature['humanstatus'] = '?/' . $feature['limit'];
// Check if we should display the bar and how.
- if (isset($feature['status']) && is_int($feature['status'])) {
- $feature['humanstatus'] = $feature['status'] . '/' . $feature['limit'];
- $feature['showbar'] = 1;
+ if (isset($feature['status'])) {
+ $feature['humanstatus'] = (string)$feature['status'];
+ if (is_int($feature['status']) && is_int($feature['limit'])) {
+ $feature['showbar'] = 1;
+ // Show currentussage with or without currentusage.
+ if (!isset($data['subscription']['currentusage'])) {
+ $data['subscription']['currentusage'] = [];
+ }
+ $feature['humanstatus'] = $feature['status'] . ' / ' . $feature['limit'];
+ $feature['percentage'] = round(($feature['status'] / $feature['limit']) * 100);
+ if ($feature['percentage'] > 100) {
+ $feature['percentage'] = 100;
+ }
+ $feature['barclass'] = 'bg-success';
- if ($feature['status'] == $feature['limit']) {
- $feature['barclass'] = 'bg-warning';
- }
-
- if ($feature['status'] > $feature['limit']) {
- $feature['barclass'] = 'bg-danger';
- $feature['humanstatus'] .= ' - ' . get_string('subscriptionlimitsurpassed', 'tool_mobile');
+ if ($feature['status'] >= ($feature['limit'] / 2)) {
+ $feature['barclass'] = 'bg-warning';
+ }
+ if ($feature['status'] >= $feature['limit']) {
+ $feature['barclass'] = 'bg-danger';
+ if ($feature['status'] > $feature['limit']) {
+ $feature['humanstatus'] .= ' - ' . get_string('subscriptionlimitsurpassed', 'tool_mobile');
+ }
+ }
+ } else {
+ $feature['showbar'] = 0;
+ if ($feature['status'] >= $feature['limit']) {
+ $feature['barclass'] = 'bg-danger';
+ if ($feature['status'] > $feature['limit']) {
+ $feature['humanstatus'] .= ' - ' . get_string('subscriptionlimitsurpassed', 'tool_mobile');
+ }
+ }
}
}
-
+ $subscribedfeatures[$feature['name']]['subscribedlimitstr'] = $feature['limitstr'];
+ $subscribedfeatures[$feature['name']]['subscribedenabled'] = $feature['enabled'];
+ $subscribedfeatures[$feature['name']]['subscribeddecoration'] =
+ $feature['enabled'] ? 'text-primary' : 'text-danger';
+ $subscribedfeatures[$feature['name']]['subscribedlimitstr'] = $feature['limitstr'];
+ $subscribedfeatures[$feature['name']]['subscribedenabled'] = $feature['enabled'];
+ $subscribedfeatures[$feature['name']]['subscribeddecoration'] =
+ $feature['enabled'] ? 'text-primary' : 'text-danger';
} else {
$feature['humanstatus'] = empty($feature['enabled']) ? get_string('notincluded') : get_string('included');
@@ -206,6 +368,14 @@ class subscription implements \renderable, \templatable {
break;
}
}
+ $subscribedfeatures[$feature['name']]['subscribedhumanstatus'] = $feature['humanstatus'];
+ $subscribedfeatures[$feature['name']]['subscribedenabled'] = $feature['enabled'] ? 'fa-check' : 'fa-times';
+ $subscribedfeatures[$feature['name']]['subscribeddecoration'] =
+ $feature['enabled'] ? 'text-primary' : 'text-danger';
+ $subscribedfeatures[$feature['name']]['subscribedhumanstatus'] = $feature['humanstatus'];
+ $subscribedfeatures[$feature['name']]['subscribedenabled'] = $feature['enabled'] ? 'fa-check' : 'fa-times';
+ $subscribedfeatures[$feature['name']]['subscribeddecoration'] =
+ $feature['enabled'] ? 'text-primary' : 'text-danger';
}
}
@@ -221,6 +391,34 @@ class subscription implements \renderable, \templatable {
}
);
+ if (!$data['availableplans']) {
+ return $data;
+ }
+ // Review availableplans features and filter to use premium.
+ foreach ($data['tocompareplan']['features'] as &$feature) {
+ if (array_key_exists('limit', $feature) && empty($feature['limit'])) {
+ $feature['limit'] = 'unlimited';
+ }
+ $feature['humanstatus'] = $feature['enabled'] ? get_string('included') : get_string('notincluded');
+ if (isset($subscribedfeatures[$feature['name']])) {
+ $feature = array_merge($feature, $subscribedfeatures[$feature['name']]);
+ }
+ }
+ if (!empty($data['tocompareplan'])) {
+ usort(
+ $data['tocompareplan']['features'],
+ function (array $featurea, array $featureb) {
+ $isfeaturea = !array_key_exists('limit', $featurea);
+ $isfeatureb = !array_key_exists('limit', $featureb);
+
+ if (!$isfeaturea && $isfeatureb) {
+ return 1;
+ }
+ return 0;
+ }
+ );
+ }
+ unset($data['availableplans']);
return $data;
}
}
diff --git a/public/admin/tool/mobile/classes/task/refresh_subscription_cache.php b/public/admin/tool/mobile/classes/task/refresh_subscription_cache.php
new file mode 100644
index 00000000000..5007ef568f3
--- /dev/null
+++ b/public/admin/tool/mobile/classes/task/refresh_subscription_cache.php
@@ -0,0 +1,64 @@
+.
+
+/**
+ * Moodle Mobile tools refresh subscription cache task definitions.
+ *
+ * @package tool_mobile
+ * @copyright 2026 Daniel Urena
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_mobile\task;
+
+use tool_mobile\api;
+
+/**
+ * Scheduled task to refresh Moodle app subscription information cache.
+ *
+ * @package tool_mobile
+ * @copyright 2026
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class refresh_subscription_cache extends \core\task\scheduled_task {
+ /**
+ * Return the task name.
+ *
+ * @return string
+ */
+ public function get_name() {
+ return get_string('taskrefreshsubscriptioncache', 'tool_mobile');
+ }
+
+ /**
+ * Execute the task.
+ */
+ public function execute() {
+ global $CFG;
+
+ if (empty($CFG->enablemobilewebservice)) {
+ mtrace('tool_mobile: task not running, mobile app is not enabled.');
+ return;
+ }
+
+ $data = api::get_subscription_information(false, true);
+ if ($data === null) {
+ mtrace('tool_mobile: subscription cache refresh failed.');
+ } else {
+ mtrace('tool_mobile: subscription cache refreshed.');
+ }
+ }
+}
diff --git a/public/admin/tool/mobile/classes/task/refresh_subscription_cache_adhoc.php b/public/admin/tool/mobile/classes/task/refresh_subscription_cache_adhoc.php
new file mode 100644
index 00000000000..9eef21a7a64
--- /dev/null
+++ b/public/admin/tool/mobile/classes/task/refresh_subscription_cache_adhoc.php
@@ -0,0 +1,59 @@
+.
+
+/**
+ * Adhoc task to refresh Moodle app subscription information cache.
+ *
+ * Used by upgrade step 2026020300 to refresh the cache once after
+ * installing or upgrading the plugin.
+ *
+ * @package tool_mobile
+ * @copyright 2026 Daniel Urena
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_mobile\task;
+
+use tool_mobile\api;
+
+/**
+ * Adhoc task to refresh Moodle app subscription information cache.
+ *
+ * @package tool_mobile
+ * @copyright 2026
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class refresh_subscription_cache_adhoc extends \core\task\adhoc_task {
+ /**
+ * Execute the task.
+ */
+ public function execute() {
+ global $CFG;
+
+ if (empty($CFG->enablemobilewebservice)) {
+ mtrace('tool_mobile: adhoc task not running, mobile app is not enabled.');
+ return;
+ }
+
+ // Force a fresh request to the Apps Portal, ignoring any cached value.
+ $data = api::get_subscription_information(false, true);
+ if ($data === null) {
+ mtrace('tool_mobile: adhoc subscription cache refresh failed.');
+ } else {
+ mtrace('tool_mobile: adhoc subscription cache refreshed.');
+ }
+ }
+}
diff --git a/public/admin/tool/mobile/db/caches.php b/public/admin/tool/mobile/db/caches.php
index 1aeed18c678..0c589d48cbb 100644
--- a/public/admin/tool/mobile/db/caches.php
+++ b/public/admin/tool/mobile/db/caches.php
@@ -31,9 +31,9 @@ $definitions = array(
'staticacceleration' => true,
'staticaccelerationsize' => 1
),
- 'subscriptiondata' => array(
- 'mode' => cache_store::MODE_SESSION,
+ 'subscriptioninfo' => [
+ 'mode' => cache_store::MODE_APPLICATION,
'simplekeys' => true,
'simpledata' => false,
- ),
+ ],
);
diff --git a/public/admin/tool/mobile/db/tasks.php b/public/admin/tool/mobile/db/tasks.php
new file mode 100644
index 00000000000..17410fe87cf
--- /dev/null
+++ b/public/admin/tool/mobile/db/tasks.php
@@ -0,0 +1,37 @@
+.
+
+/**
+ * Moodle Mobile tools tasks definitions.
+ *
+ * @package tool_mobile
+ * @copyright 2026 Daniel Urena
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$tasks = [
+ [
+ 'classname' => 'tool_mobile\task\refresh_subscription_cache',
+ 'blocking' => 0,
+ 'minute' => 'R',
+ 'hour' => 'R',
+ 'day' => '*',
+ 'dayofweek' => 'R',
+ 'month' => '*',
+ ],
+];
diff --git a/public/admin/tool/mobile/db/upgrade.php b/public/admin/tool/mobile/db/upgrade.php
index be4536edd40..d0d58c648b0 100644
--- a/public/admin/tool/mobile/db/upgrade.php
+++ b/public/admin/tool/mobile/db/upgrade.php
@@ -41,5 +41,11 @@ function xmldb_tool_mobile_upgrade($oldversion) {
// Automatically generated Moodle v5.1.0 release upgrade line.
// Put any upgrade step following this.
+ if ($oldversion < 2026012700) {
+ // Run the subscription cache refresh task as soon as possible after upgrade by queueing an adhoc task.
+ $task = new \tool_mobile\task\refresh_subscription_cache_adhoc();
+ \core\task\manager::queue_adhoc_task($task, true);
+ upgrade_plugin_savepoint(true, 2026012700, 'tool', 'mobile');
+ }
return true;
}
diff --git a/public/admin/tool/mobile/lang/en/tool_mobile.php b/public/admin/tool/mobile/lang/en/tool_mobile.php
index 19dd8c6f9f8..23c79f518e2 100644
--- a/public/admin/tool/mobile/lang/en/tool_mobile.php
+++ b/public/admin/tool/mobile/lang/en/tool_mobile.php
@@ -38,10 +38,12 @@ $string['autologout_desc'] = 'For security reasons, you can enforce automatic lo
$string['autologoutcustom'] = 'Custom time after users leave or close the app';
$string['autologoutinmediate'] = 'Immediately after users leave or close the app';
$string['autologouttime'] = 'Auto logout timer';
+$string['billedannually'] = 'Billed annually';
$string['cachedef_plugininfo'] = 'This stores the list of plugins with mobile addons';
-$string['cachedef_subscriptiondata'] = 'This stores the Moodle app subscription information.';
+$string['cachedef_subscriptioninfo'] = 'This stores the Moodle app subscription information.';
$string['clickheretolaunchtheapp'] = 'Click here if the app does not open automatically.';
$string['configmobilecssurl'] = 'A CSS file to customise your mobile app interface.';
+$string['currentusage'] = 'Current usage';
$string['customlangstrings'] = 'Custom language strings';
$string['customlangstrings_desc'] = 'Words and phrases displayed in the app can be customised here. Enter each custom language string on a new line with format: string identifier, custom language string and language code, separated by pipe characters. For example:
@@ -78,6 +80,7 @@ Mis calificaciones|https://someurl.xyz/local/mygrades/index.php|embedded|es
You will only see this in English|https://someurl.xyz/english|browser|en_only
';
$string['darkmode'] = 'Dark mode';
+$string['devicelimitnotrack'] = 'Per device limit (no usage tracking)';
$string['disabledfeatures'] = 'Disabled features';
$string['disabledfeatures_desc'] = 'Select here the features you want to disable in the Mobile app for your site. Please note that some features listed here could be already disabled via other site settings. You will have to log out and log in again in the app to see the changes.';
$string['displayerrorswarning'] = 'Display debug messages (debugdisplay) is enabled. It should be disabled.';
@@ -85,6 +88,9 @@ $string['downloadcourse'] = 'Download course';
$string['downloadcourses'] = 'Download courses';
$string['enablesmartappbanners'] = 'Enable App Banners';
$string['enablesmartappbanners_desc'] = 'If enabled, a banner promoting the mobile app will be displayed when accessing the site using a mobile browser.';
+$string['feature'] = 'Feature';
+$string['featurecomparison'] = 'Feature comparison';
+$string['features'] = 'features';
$string['filetypeexclusionlist'] = 'File type exclusion list';
$string['filetypeexclusionlist_desc'] = 'Select all file types which are not for use on a mobile device. Such files will be listed in the course, then if a user attempts to open them, a warning will be displayed advising that the file type is not intended for use on a mobile device. The user can then cancel or ignore the warning and open the file anyway.';
$string['filetypeexclusionlistplaceholder'] = 'Mobile file type exclusion list';
@@ -92,6 +98,8 @@ $string['forcedurlscheme'] = 'If you want to allow only your custom branded app
$string['forcedurlscheme_key'] = 'URL scheme';
$string['forcelogout'] = 'Force log out';
$string['forcelogout_desc'] = 'If enabled, users will be always completely logged out even when switching accounts. They must then re-enter their password the next time they wish to access the site.';
+$string['fullaccessfeatures'] = 'You have full access to {$a} features';
+$string['getmoodleonyourmobile'] = 'Get the mobile app';
$string['h5poffline'] = 'View H5P content offline';
$string['httpsrequired'] = 'HTTPS required';
$string['insecurealgorithmwarning'] = 'It seems that the HTTPS certificate uses an insecure algorithm for signing (SHA-1). Please try updating the certificate.';
@@ -103,17 +111,19 @@ $string['invaliduserquotawarning'] = 'The user quota (userquota) is set to an in
$string['iosappid'] = 'iOS app\'s unique identifier';
$string['iosappid_desc'] = 'This setting may be left as default unless you have a custom iOS app.';
$string['launchviasiteinbrowser'] = 'Launch via site in system browser';
+$string['limitedfeatures'] = 'Limited features';
$string['loginintheapp'] = 'Via the app';
$string['logininthebrowser'] = 'Via a browser window (for SSO plugins)';
$string['loginintheembeddedbrowser'] = 'Via an embedded browser (for SSO plugins)';
$string['logoutconfirmation'] = 'Are you sure you want to log out from the mobile app on your mobile devices? By logging out, you will then need to re-enter your username and password in the mobile app on all devices where you have the app installed.';
$string['mainmenu'] = 'Main menu';
+$string['manage'] = 'Manage';
$string['managefiletypes'] = 'Manage file types';
$string['minimumversion'] = 'If an app version is specified (3.8.0 or higher), any users using an older app version will be prompted to upgrade their app before being allowed access to the site.';
$string['minimumversion_key'] = 'Minimum app version required';
$string['mobileapp'] = 'Mobile app';
-$string['mobileappenabled'] = 'This site has mobile app access enabled. Download the mobile app.';
$string['mobileappearance'] = 'Mobile appearance';
+$string['mobileappenabled'] = 'This site has mobile app access enabled. Download the mobile app.';
$string['mobileappsubscription'] = 'Moodle app subscription';
$string['mobileauthentication'] = 'Mobile authentication';
$string['mobilecssurl'] = 'CSS';
@@ -121,38 +131,55 @@ $string['mobilefeatures'] = 'Mobile features';
$string['mobilenotificationsdisabledwarning'] = 'Mobile notifications are not enabled. They should be enabled in Notification settings.';
$string['mobilesettings'] = 'Mobile settings';
$string['moodleappsportalfeatureswarning'] = 'Please note that some features may be restricted depending on your Moodle app subscription. For details, visit the Moodle Apps Portal.';
+$string['nosubsblocked'] = 'Your Moodle server may be blocking outbound connections to the Moodle Apps Portal.';
+$string['nosubscouldntretrieve'] = "We couldn't retrieve your plan details";
+$string['nosubsoerror'] = "Unable to connect to Moodle Apps Portal. Your plan, feature limits, and usage data are temporarily unavailable. This doesnโt affect your active subscription.";
+$string['nosubstryagain'] = "Try again";
+$string['nosubsviewplan'] = "View your plan on the Moodle Apps Portal";
+$string['nosubswhyhappen'] = "Why might this happen?";
$string['notifications'] = 'Notifications';
$string['notificationsactivedevices'] = 'Active devices';
+$string['notificationscurrentactivedevices'] = 'Devices receiving notifications this month';
$string['notificationsignorednotifications'] = 'Notifications not sent';
-$string['notificationslimitreached'] = 'The monthly active user devices limit has been exceeded. Notifications for some users will not be sent. It is recommended that you upgrade your app plan in the Moodle Apps Portal.';
-$string['notificationsmissingwarning'] = 'Moodle app notification statistics could not be retrieved. This is most likely because mobile notifications are not yet enabled on the site. You can enable them in Site Administration / Messaging / Mobile.';
+$string['notificationslimitreached'] = 'Monthly device limit reached. Some users are not receiving push notifications. Upgrade your plan to ensure all students and staff receive mobile notifications.';
+$string['notificationsmissingwarning'] = 'Moodle app notification statistics could not be retrieved. This is most likely because mobile notifications are not yet enabled on the site. You can enable them in Site Administration / Messaging / Mobile.';
$string['notificationsnewdevices'] = 'New devices';
$string['notificationsseemore'] = 'Note: Moodle app usage statistics are not calculated in real time. To access more detailed statistics, including data from previous months, please log in to the Moodle Apps Portal.';
$string['notificationssentnotifications'] = 'Notifications sent';
-$string['notificationscurrentactivedevices'] = 'Devices receiving notifications this month';
$string['oauth2identityproviders'] = 'OAuth 2 identity providers';
$string['offlineuse'] = 'Offline use';
+$string['planbenefits'] = 'Your {$a} plan benefits';
+$string['plancontinues'] = 'Your {$a} plan continues without changes';
+$string['plandetails'] = 'Your Moodle app subscription';
+$string['planexpireson'] = 'Expires on ';
+$string['planneverchecked'] = 'Plan status has never been checked.';
$string['pluginname'] = 'Moodle app tools';
$string['pluginnotenabledorconfigured'] = 'Plugin not enabled or configured.';
+$string['privacy:metadata:core_userkey'] = 'User\'s keys used to create auto-login key for the current user.';
+$string['privacy:metadata:preference:tool_mobile_autologin_request_last'] = 'The date of the last auto-login key request. Between each request 6 minutes are required.';
$string['qrcodedisabled'] = 'Access via QR code disabled';
$string['qrcodeformobileappaccess'] = 'QR code for mobile app access';
$string['qrcodeformobileapploginabout'] = 'Scan the QR code with your mobile app and you will be automatically logged in. The QR code will expire in {$a}.';
$string['qrcodeformobileappurlabout'] = 'Scan the QR code with your mobile app to fill in the site URL in your app.';
-$string['qrsiteadminsnotallowed'] = 'For security reasons login via QR code is not allowed for site administrators or if you are logged in as another user.';
$string['qrcodetype'] = 'QR code access';
$string['qrcodetype_desc'] = 'A QR code can be provided for mobile app users to scan. This can be used to fill in the site URL, or where the site is secured using HTTPS, to automatically log the user in without having to enter their username and password.';
-$string['qrcodetypeurl'] = 'QR code with site URL';
$string['qrcodetypelogin'] = 'QR code with automatic login';
+$string['qrcodetypeurl'] = 'QR code with site URL';
$string['qrkeyttl'] = 'QR authentication key duration';
$string['qrkeyttl_desc'] = 'The length of time for which a QR code for automatic login is valid.';
$string['qrsameipcheck'] = 'QR authentication same IP check';
$string['qrsameipcheck_desc'] = 'Whether users must use the same network for both generating and scanning a QR code for login. Only disable it if users report issues with the QR login.';
+$string['qrsiteadminsnotallowed'] = 'For security reasons login via QR code is not allowed for site administrators or if you are logged in as another user.';
$string['readingthisemailgettheapp'] = 'Are you reading this in an email? Download the mobile app and receive notifications on your mobile device.';
+$string['register'] = 'Register';
$string['remoteaddons'] = 'Remote add-ons';
+$string['responsivemainmenuitems'] = 'Responsive menu items';
$string['scanqrcode'] = 'Scan QR code';
$string['selfsignedoruntrustedcertificatewarning'] = 'It seems that the HTTPS certificate is self-signed or not trusted. The mobile app will only work with trusted sites. Please use any online SSL checker to diagnose the problem. If it indicates that your certificate is OK, you can ignore this warning.';
$string['setuplink'] = 'App download page';
$string['setuplink_desc'] = 'URL of page with options to download the mobile app from the App Store and Google Play. The app download page link is displayed in the page footer and in a user\'s profile. Leave blank to not display a link.';
+$string['showingcacheddata'] = 'Last synced: {$a->since} days ago ({$a->date}).';
+$string['showinglivedata'] = 'View live data in the Moodle Apps Portal.';
$string['smartappbanners'] = 'App Banners';
$string['subscription'] = 'Subscription';
$string['subscriptioncreated'] = 'Start date';
@@ -163,11 +190,11 @@ $string['subscriptionfeatures'] = 'Subscription features';
$string['subscriptionlimitsurpassed'] = 'Subscription limit exceeded';
$string['subscriptionregister'] = 'For details of the various app plans, and to access Moodle app usage statistics, please visit the Moodle Apps Portal.';
$string['subscriptionsseemore'] = 'Note: The information displayed is not updated in real time. You may need to log out and log in again to see updates. For information on upgrading your app plan, please log in to the Moodle Apps Portal.';
+$string['switchaccount'] = 'Switch account';
+$string['taskrefreshsubscriptioncache'] = 'Refresh Moodle app subscription information cache';
$string['typeoflogin'] = 'Type of login';
$string['typeoflogin_desc'] = 'If the site uses a SSO authentication method, then select via a browser window or via an embedded browser. An embedded browser provides a better user experience, though it doesn\'t work with all SSO plugins.';
-$string['getmoodleonyourmobile'] = 'Get the mobile app';
-$string['privacy:metadata:preference:tool_mobile_autologin_request_last'] = 'The date of the last auto-login key request. Between each request 6 minutes are required.';
-$string['privacy:metadata:core_userkey'] = 'User\'s keys used to create auto-login key for the current user.';
-$string['responsivemainmenuitems'] = 'Responsive menu items';
-$string['switchaccount'] = 'Switch account';
+$string['upgradeforunlimit'] = 'Upgrade to {$a} for unlimited access.';
+$string['upgradetosubscription'] = 'Upgrade to {$a}';
+$string['upgradeyourplan'] = 'Upgrade your plan';
$string['viewqrcode'] = 'View QR code';
diff --git a/public/admin/tool/mobile/settings.php b/public/admin/tool/mobile/settings.php
index a355636b556..e798c633235 100644
--- a/public/admin/tool/mobile/settings.php
+++ b/public/admin/tool/mobile/settings.php
@@ -27,6 +27,8 @@
defined('MOODLE_INTERNAL') || die();
use core_admin\local\settings\autocomplete;
+use tool_mobile\api;
+
if ($hassiteconfig || has_capability('moodle/site:configview', context_system::instance())) {
// We should wait to the installation to finish since we depend on some configuration values that are set once
@@ -46,42 +48,60 @@ if ($hassiteconfig || has_capability('moodle/site:configview', context_system::i
new admin_category('mobileapp', new lang_string('mobileapp', 'tool_mobile'), $ismobilewsdisabled),
'development'
);
-
- $temp = new admin_settingpage('mobilesettings',
- new lang_string('mobilesettings', 'tool_mobile'),
- 'moodle/site:config',
- $ismobilewsdisabled
+ // General notification about limited features due to app restrictions.
+ $subscriptionurl = (new moodle_url("/$CFG->admin/tool/mobile/subscription.php"))->out(false);
+ $notify = new \core\output\notification(
+ get_string('moodleappsportalfeatureswarning', 'tool_mobile', $subscriptionurl),
+ \core\output\notification::NOTIFY_WARNING
);
+ $featuresnotice = $OUTPUT->render($notify);
- $temp->add(new admin_setting_configtext('tool_mobile/apppolicy', new lang_string('apppolicy', 'tool_mobile'),
- new lang_string('apppolicy_help', 'tool_mobile'), '', PARAM_URL));
+ $visiblename = get_string('mobileappsubscription', 'tool_mobile');
- $ADMIN->add('mobileapp', $temp);
-
- $featuresnotice = null;
- if (empty($CFG->disablemobileappsubscription)) {
- // General notification about limited features due to app restrictions.
- $subscriptionurl = (new moodle_url("/$CFG->admin/tool/mobile/subscription.php"))->out(false);
- $notify = new \core\output\notification(
- get_string('moodleappsportalfeatureswarning', 'tool_mobile', $subscriptionurl),
- \core\output\notification::NOTIFY_WARNING);
- $featuresnotice = $OUTPUT->render($notify);
+ $ispremiumplan = false;
+ $subscriptiondata = api::get_subscription_information(true);
+ if (is_array($subscriptiondata) && !empty($subscriptiondata['subscription']['plan'])) {
+ $plan = \core_text::strtolower(trim($subscriptiondata['subscription']['plan']));
+ $ispremiumplan = ($plan === 'premium' || $plan === 'bma');
+ }
+ if (!$ispremiumplan) {
+ $upgradetext = strtoupper(get_string('upgradeyourplan', 'tool_mobile'));
+ if (right_to_left()) {
+ $visiblename .= ' ' . $upgradetext . ' ๐';
+ } else {
+ $visiblename .= ' ๐ ' . $upgradetext;
+ }
}
-
- $hideappsubscription = (isset($CFG->disablemobileappsubscription) && !empty($CFG->disablemobileappsubscription));
- $hideappsubscription = $ismobilewsdisabled || $hideappsubscription;
$ADMIN->add(
'mobileapp',
new admin_externalpage(
'mobileappsubscription',
- new lang_string('mobileappsubscription', 'tool_mobile'),
+ $visiblename,
"$CFG->wwwroot/$CFG->admin/tool/mobile/subscription.php",
'moodle/site:configview',
- $hideappsubscription
+ $ismobilewsdisabled
)
);
+
+ $temp = new admin_settingpage(
+ 'mobilesettings',
+ new lang_string('mobilesettings', 'tool_mobile'),
+ 'moodle/site:config',
+ $ismobilewsdisabled,
+ );
+
+ $temp->add(new admin_setting_configtext(
+ 'tool_mobile/apppolicy',
+ new lang_string('apppolicy', 'tool_mobile'),
+ new lang_string('apppolicy_help', 'tool_mobile'),
+ '',
+ PARAM_URL,
+ ));
+
+ $ADMIN->add('mobileapp', $temp);
+
// Type of login.
$temp = new admin_settingpage(
'mobileauthentication',
@@ -169,9 +189,7 @@ if ($hassiteconfig || has_capability('moodle/site:configview', context_system::i
$ismobilewsdisabled
);
- if (!empty($featuresnotice)) {
- $temp->add(new admin_setting_heading('tool_mobile/moodleappsportalfeaturesappearance', '', $featuresnotice));
- }
+ $temp->add(new admin_setting_heading('tool_mobile/moodleappsportalfeaturesappearance', '', $featuresnotice));
$temp->add(new admin_setting_configtext('mobilecssurl', new lang_string('mobilecssurl', 'tool_mobile'),
new lang_string('configmobilecssurl', 'tool_mobile'), '', PARAM_URL));
@@ -210,9 +228,7 @@ if ($hassiteconfig || has_capability('moodle/site:configview', context_system::i
$ismobilewsdisabled
);
- if (!empty($featuresnotice)) {
- $temp->add(new admin_setting_heading('tool_mobile/moodleappsportalfeatures', '', $featuresnotice));
- }
+ $temp->add(new admin_setting_heading('tool_mobile/moodleappsportalfeatures', '', $featuresnotice));
$temp->add(new admin_setting_heading('tool_mobile/logout',
new lang_string('logout'), ''));
diff --git a/public/admin/tool/mobile/subscription.php b/public/admin/tool/mobile/subscription.php
index 8eca0d48431..8a9d20165d2 100644
--- a/public/admin/tool/mobile/subscription.php
+++ b/public/admin/tool/mobile/subscription.php
@@ -32,20 +32,15 @@ admin_externalpage_setup('mobileappsubscription', '', null, '');
if (!$CFG->enablemobilewebservice) {
throw new \moodle_exception('enablewsdescription', 'webservice');
}
-// Check is this feature is globaly disabled.
-if (!empty($CFG->disablemobileappsubscription)) {
- throw new \moodle_exception('disabled', 'admin');
-}
-$subscriptiondata = \tool_mobile\api::get_subscription_information();
+$subscriptiondata = \tool_mobile\api::get_subscription_information(false, true, 5);
echo $OUTPUT->header();
if (empty($subscriptiondata)) {
- echo $OUTPUT->notification(get_string('subscriptionerrorrequest', 'tool_mobile'), \core\output\notification::NOTIFY_ERROR);
-} else {
- $templatable = new \tool_mobile\output\subscription($subscriptiondata);
- echo $PAGE->get_renderer('tool_mobile')->render($templatable);
+ $subscriptiondata = [];
}
+$templatable = new \tool_mobile\output\subscription($subscriptiondata);
+echo $PAGE->get_renderer('tool_mobile')->render($templatable);
echo $OUTPUT->footer();
diff --git a/public/admin/tool/mobile/templates/subscribe_alert.mustache b/public/admin/tool/mobile/templates/subscribe_alert.mustache
new file mode 100644
index 00000000000..76aac149cc0
--- /dev/null
+++ b/public/admin/tool/mobile/templates/subscribe_alert.mustache
@@ -0,0 +1,71 @@
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see .
+}}
+{{!
+ @template tool_mobile/subscribe_alert
+
+ Subscription-related alert notification for the Mobile tool.
+
+ The purpose of this template is to render an alert with an
+ optional action button (for example, to register the site or
+ complete subscription details).
+
+ Classes required for JS:
+ * none
+
+ Data attributes required for JS:
+ * none
+
+ Context variables required for this template:
+ * message A cleaned string (use clean_text()) to display.
+ * extraclasses Additional classes to apply to the notification.
+ * buttonstr Text for the optional action button.
+ * buttonurl URL for the optional action button.
+ * extrabuttonclasses Additional classes for the button element.
+ * icon Optional icon definition (with "icon" and "component").
+
+ Example context (json):
+ {
+ "message": "Your site is not registered in Moodle Apps Portal.",
+ "extraclasses": "alert-danger",
+ "buttonstr": "Register",
+ "buttonurl": "https://apps.moodle.com/register",
+ "extrabuttonclasses": "bg-danger",
+ "icon": {
+ "icon": "i/circleinfo",
+ "component": "core"
+ }
+ }
+}}
+
+