diff --git a/Api/Data/UpdateNotificationInterface.php b/Api/Data/UpdateNotificationInterface.php new file mode 100644 index 0000000..02e1f6c --- /dev/null +++ b/Api/Data/UpdateNotificationInterface.php @@ -0,0 +1,34 @@ +_scopeConfig = $context->getScopeConfig(); @@ -50,8 +65,14 @@ public function __construct( $this->_resourceConfig = $resourceConfig; $this->_cacheTypeList = $cacheTypeList; $this->_cacheFrontendPool = $cacheFrontendPool; - $cachedData = $this->_getCachedData(); - $this->accountInfo = $cachedData['accountInfo']; + $this->_serializer = $serializer; + $this->_dataHelper = $dataHelper; + $this->_updateNotifier = $updateNotifier; + + $this->_cachedData = $this->_getCachedData(); + + $this->_notifyUpdate(); + parent::__construct($context, $data); } @@ -78,8 +99,8 @@ public function render(AbstractElement $element): string public function getConfig(): array { return [ - 'enabled' => $this->_storeConfigHelper->isSetFlag(StoreConfigHelper::PATH['enabled']), - 'module_version' => $this->_storeConfigHelper->getValue(StoreConfigHelper::PATH['module_version']), + 'enabled' => $this->_storeConfigHelper->isEnabled(), + 'module_version' => $this->_storeConfigHelper->getModuleVersion(), 'supported_countries' => $this->_storeConfigHelper->getSupportedCountries(), 'account_name' => $this->_storeConfigHelper->getValue(StoreConfigHelper::PATH['account_name']), 'account_status' => $this->_storeConfigHelper->getValue(StoreConfigHelper::PATH['account_status']), // Defaults to "new", see etc/config.xml. @@ -87,6 +108,26 @@ public function getConfig(): array ]; } + /** + * Get cached account info. + * + * @return array + */ + public function getAccountInfo(): array + { + return $this->_cachedData['accountInfo'] ?? []; + } + + /** + * Get cached module info. + * + * @return array + */ + public function getModuleInfo(): array + { + return $this->_cachedData['moduleInfo'] ?? []; + } + /** * Get short description of API status. * @@ -110,6 +151,27 @@ public function getApiStatusDescription(): string } } + /** + * Get cached data. + * + * @return array + */ + private function _getCachedData(): array + { + $cache = $this->_cacheFrontendPool->get(\Magento\Framework\App\Cache\Type\Config::TYPE_IDENTIFIER); + $cachedData = $cache->load(self::CACHE_ID); + + if ($cachedData === false) { + $data = []; + $data['accountInfo'] = $this->_getAccountInfo(); + $data['moduleInfo'] = $this->_dataHelper->getModuleInfo(); + $cache->save($this->_serializer->serialize($data), self::CACHE_ID, [], self::CACHE_LIFETIME_SECONDS); + return $data; + } + + return $this->_serializer->unserialize($cachedData); + } + /** * Get Postcode.eu API account info. * @@ -118,30 +180,21 @@ public function getApiStatusDescription(): string private function _getAccountInfo(): array { $status = $this->_storeConfigHelper->getValue(StoreConfigHelper::PATH['account_status']); - if ($status === \Flekto\Postcode\Helper\ApiClientHelper::API_ACCOUNT_STATUS_ACTIVE) + if ($status === \Flekto\Postcode\Helper\ApiClientHelper::API_ACCOUNT_STATUS_ACTIVE) { return $this->_apiClientHelper->getApiClient()->accountInfo(); + } return []; } /** - * Get cached data. - * - * @return array + * Set a notification if an update is available. */ - private function _getCachedData(): array + private function _notifyUpdate(): void { - $cache = $this->_cacheFrontendPool->get(\Magento\Framework\App\Cache\Type\Config::TYPE_IDENTIFIER); - $cachedData = $cache->load(self::CACHE_ID); - - if ($cachedData === false) - { - $data = []; - $data['accountInfo'] = $this->_getAccountInfo(); - $cache->save(serialize($data), self::CACHE_ID, [], self::CACHE_LIFETIME_SECONDS); - return $data; + $moduleInfo = $this->getModuleInfo(); + if ($moduleInfo['has_update'] ?? false) { + $this->_updateNotifier->notifyVersion($moduleInfo['latest_version']); } - - return unserialize($cachedData); } } diff --git a/Cron/NotifyModuleUpdate.php b/Cron/NotifyModuleUpdate.php new file mode 100644 index 0000000..3f67fb8 --- /dev/null +++ b/Cron/NotifyModuleUpdate.php @@ -0,0 +1,49 @@ +_logger = $logger; + $this->_dataHelper = $dataHelper; + $this->_updateNotifier = $updateNotifier; + } + + /** + * Run cron job. + * + * @access public + * @return void + */ + public function execute(): void + { + $moduleInfo = $this->_dataHelper->getModuleInfo(); + if (($moduleInfo['has_update'] ?? false) + && $this->_updateNotifier->notifyVersion($moduleInfo['latest_version']) + ) { + $this->_logger->info(__('Added notification for Postcode.eu Address API %1 update.', $moduleInfo['latest_version'])); + } + } +} diff --git a/Cron/UpdateApiData.php b/Cron/UpdateApiData.php index 0630177..fd87282 100644 --- a/Cron/UpdateApiData.php +++ b/Cron/UpdateApiData.php @@ -15,7 +15,7 @@ class UpdateApiData protected $_storeConfigHelper; /** - * __construct function. + * Constructor * * @access public * @param LoggerInterface $logger diff --git a/Helper/Data.php b/Helper/Data.php index 933c56a..abcd63f 100644 --- a/Helper/Data.php +++ b/Helper/Data.php @@ -7,27 +7,58 @@ use Flekto\Postcode\Model\Config\Source\ShowHideAddressFields; use Magento\Framework\App\Helper\AbstractHelper; use Magento\Framework\App\Helper\Context; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem\DirectoryList; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\Framework\HTTP\Client\Curl; class Data extends AbstractHelper { + public const MODULE_RELEASE_URL = 'https://github.com/postcode-nl/PostcodeNl_Api_Magento2/releases/latest'; + public const PACKAGIST_URL = 'https://repo.packagist.org/p2/postcode-nl/api-magento2-module.json'; + /** * @var StoreConfigHelper */ private $_storeConfigHelper; + /** + * @var DirectoryList + */ + private $_dir; + + /** + * @var DriverInterface + */ + private $_fs; + + /** + * @var Curl + */ + private $_curl; + /** * Constructor * * @access public * @param Context $context * @param StoreConfigHelper $storeConfigHelper + * @param DirectoryList $dir + * @param DriverInterface $filesystem + * @param Curl $curl * @return void */ public function __construct( Context $context, - StoreConfigHelper $storeConfigHelper + StoreConfigHelper $storeConfigHelper, + DirectoryList $dir, + DriverInterface $filesystem, + Curl $curl, ) { $this->_storeConfigHelper = $storeConfigHelper; + $this->_dir = $dir; + $this->_fs = $filesystem; + $this->_curl = $curl; parent::__construct($context); } @@ -37,7 +68,7 @@ public function __construct( * @access public * @return bool */ - public function isFormattedOutputDisabled() + public function isFormattedOutputDisabled(): bool { return $this->isDisabled() @@ -50,7 +81,7 @@ public function isFormattedOutputDisabled() * @access public * @return bool */ - public function isNlComponentDisabled() + public function isNlComponentDisabled(): bool { return $this->isDisabled() @@ -64,7 +95,7 @@ public function isNlComponentDisabled() * @access public * @return bool */ - public function isDisabled() + public function isDisabled(): bool { return false === $this->_storeConfigHelper->isSetFlag(StoreConfigHelper::PATH['enabled']) @@ -77,11 +108,94 @@ public function isDisabled() * @access public * @return bool */ - public function isAutofillBypassDisabled() + public function isAutofillBypassDisabled(): bool { return $this->isDisabled() || ShowHideAddressFields::SHOW == $this->_storeConfigHelper->getValue(StoreConfigHelper::PATH['show_hide_address_fields']) || $this->_storeConfigHelper->isSetFlag(StoreConfigHelper::PATH['allow_autofill_bypass']) === false; } + + /** + * Get module info. + * + * @return array + */ + public function getModuleInfo(): array + { + $version = $this->_storeConfigHelper->getModuleVersion(); + + try { + $data = $this->_getPackageData(); + $latest_version = $data['packages']['postcode-nl/api-magento2-module'][0]['version']; + } catch (LocalizedException $e) { + $this->_logger->error(__('Failed to get package data: "%1".', $e->getMessage())); + $latest_version = $version; + } + + return [ + 'version' => $version, + 'latest_version' => $latest_version, + 'has_update' => version_compare($latest_version, $version, '>'), + 'release_url' => $this->getModuleReleaseUrl(), + ]; + } + + /** + * Request module info from Packagist. + * + * Will only download from Packagist if their file is newer. + * + * @throws LocalizedException + * @return array - Decoded JSON data. + */ + private function _getPackageData(): array + { + $path = $this->_dir->getPath('var') . '/Flekto_Postcode'; + if (!$this->_fs->isDirectory($path)) { + $this->_fs->createDirectory($path, 0755); + } + + $filePath = $path . '/package-data.json'; + if ($this->_fs->isExists($filePath)) { + $lastModified = $this->_fs->stat($filePath)['mtime']; + + if ($lastModified !== false) { + $this->_curl->setHeaders(['If-Modified-Since' => gmdate('D, d M Y H:i:s T', $lastModified)]); + } + } + + $this->_curl->get(self::PACKAGIST_URL); + $status = $this->_curl->getStatus(); + if ($status == 200) { + $response = $this->_curl->getBody(); + + if ($this->_fs->filePutContents($filePath, $response) === false) { + throw new LocalizedException(__('Failed to write package data to %1.', $filePath)); + } + + return json_decode($response, true); + + } elseif ($status == 304) { // Not modified, use cached file. + $data = $this->_fs->fileGetContents($filePath); + + if ($data === false) { + throw new LocalizedException(__('Failed to read package data from %1.', $filePath)); + } + + return json_decode($data, true); + } + + throw new LocalizedException(__('Unexpected status code %1 while fetching package data.', $status)); + } + + /** + * Get URL to the latest version of the module. + * + * @return string + */ + public function getModuleReleaseUrl(): string + { + return self::MODULE_RELEASE_URL; + } } diff --git a/Helper/ModuleHelper.php b/Helper/ModuleHelper.php new file mode 100644 index 0000000..e69de29 diff --git a/Helper/StoreConfigHelper.php b/Helper/StoreConfigHelper.php index 8c92fd2..93e65e0 100644 --- a/Helper/StoreConfigHelper.php +++ b/Helper/StoreConfigHelper.php @@ -105,4 +105,15 @@ public function hasCredentials(): bool return isset($key, $secret); } + + /** + * Get current module version. + * + * @access public + * @return string + */ + public function getModuleVersion(): string + { + return $this->getValue(static::PATH['module_version']); + } } diff --git a/Model/ResourceModel/UpdateNotification.php b/Model/ResourceModel/UpdateNotification.php new file mode 100644 index 0000000..542e35c --- /dev/null +++ b/Model/ResourceModel/UpdateNotification.php @@ -0,0 +1,16 @@ +_init(self::MAIN_TABLE, self::ID_FIELD); + } +} diff --git a/Model/ResourceModel/UpdateNotification/Collection.php b/Model/ResourceModel/UpdateNotification/Collection.php new file mode 100644 index 0000000..39a136a --- /dev/null +++ b/Model/ResourceModel/UpdateNotification/Collection.php @@ -0,0 +1,18 @@ +_init( + UpdateNotification::class, + UpdateNotificationResourceModel::class + ); + } +} diff --git a/Model/UpdateNotification.php b/Model/UpdateNotification.php new file mode 100644 index 0000000..b2d8ce4 --- /dev/null +++ b/Model/UpdateNotification.php @@ -0,0 +1,34 @@ +_init(ResourceModel\UpdateNotification::class); + } + + public function getVersion(): string + { + return $this->getData(self::VERSION); + } + + public function setVersion(string $version): UpdateNotification + { + return $this->setData(self::VERSION, $version); + } + + public function getNotified(): bool + { + return $this->getData(self::NOTIFIED); + } + + public function setNotified(bool $notified): UpdateNotification + { + return $this->setData(self::NOTIFIED, $notified); + } +} diff --git a/Model/UpdateNotification/UpdateNotifier.php b/Model/UpdateNotification/UpdateNotifier.php new file mode 100644 index 0000000..ac95c33 --- /dev/null +++ b/Model/UpdateNotification/UpdateNotifier.php @@ -0,0 +1,52 @@ +_notifier = $notifier; + $this->_updateNotification = $updateNotification; + } + + /** + * Notifies about a new version. + * + * @param string $version + * @return bool - True if notified about a new version, false otherwise. + */ + public function notifyVersion(string $version): bool + { + if ($this->_updateNotification->isVersionNotified($version)) { + return false; + } + + $this->_notifier->addNotice( + __('Postcode.eu Address API update available'), + __('Stay ahead with our latest update. + Get the newest features and improvements for our Postcode.eu address validation module.'), + \Flekto\Postcode\Helper\Data::MODULE_RELEASE_URL + ); + $this->_updateNotification->setVersionNotified($version); + return $this->_updateNotification->isVersionNotified($version); + } +} diff --git a/Model/UpdateNotificationRepository.php b/Model/UpdateNotificationRepository.php new file mode 100644 index 0000000..e7b5174 --- /dev/null +++ b/Model/UpdateNotificationRepository.php @@ -0,0 +1,71 @@ +_resource = $resource; + $this->_notificationFactory = $notificationFactory; + } + + public function getByVersion(string $version): UpdateNotificationInterface + { + $notification = $this->_notificationFactory->create(); + $this->_resource->load($notification, $version, 'version'); + + if (!$notification->getId()) { + throw new NoSuchEntityException(__('Version "%1" not found', $version)); + } + + return $notification; + } + + public function save(UpdateNotificationInterface $notification): UpdateNotificationInterface + { + try { + $this->_resource->save($notification); + } catch (\Exception $e) { + throw new CouldNotSaveException(__($e->getMessage())); + } + + return $notification; + } + + public function setVersionNotified(string $version): void + { + try { + $notification = $this->getByVersion($version); + } catch (NoSuchEntityException $e) { + $notification = $this->_notificationFactory->create(); + } + + $notification->setVersion($version); + $notification->setNotified(true); + $this->_resource->save($notification); + } + + public function isVersionNotified(string $version): bool + { + try { + $notification = $this->getByVersion($version); + return $notification->getNotified(); + } catch (NoSuchEntityException $e) { + return false; + } + } +} diff --git a/Observer/System/Config.php b/Observer/System/Config.php index fba6083..8bfb0b7 100644 --- a/Observer/System/Config.php +++ b/Observer/System/Config.php @@ -32,6 +32,7 @@ class Config implements ObserverInterface * @param CacheFrontendPool $cacheFrontendPool * @param ApiClientHelper $apiClientHelper * @param StoreConfigHelper $storeConfigHelper + * @param RequestInterface $request * @return void */ public function __construct( @@ -123,6 +124,7 @@ public function execute(Observer $observer): void /** * Clean config cache. + * * @return void */ protected function _cleanConfigCache(): void diff --git a/etc/adminhtml/di.xml b/etc/adminhtml/di.xml index ef8c61d..ce566c1 100644 --- a/etc/adminhtml/di.xml +++ b/etc/adminhtml/di.xml @@ -1,5 +1,4 @@ - diff --git a/etc/config.xml b/etc/config.xml index 467dc38..8bafb93 100644 --- a/etc/config.xml +++ b/etc/config.xml @@ -3,7 +3,7 @@ - 3.1.10 + 3.1.11 new diff --git a/etc/crontab.xml b/etc/crontab.xml index a948717..9144a4a 100644 --- a/etc/crontab.xml +++ b/etc/crontab.xml @@ -4,5 +4,8 @@ 3 3 * * * + + 3 3 * * * + diff --git a/etc/db_schema.xml b/etc/db_schema.xml new file mode 100644 index 0000000..2be74f0 --- /dev/null +++ b/etc/db_schema.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + +
+
diff --git a/etc/di.xml b/etc/di.xml index 6f78329..63e31b7 100644 --- a/etc/di.xml +++ b/etc/di.xml @@ -1,6 +1,9 @@ + + + diff --git a/etc/module.xml b/etc/module.xml index 26ad5ed..87d584a 100644 --- a/etc/module.xml +++ b/etc/module.xml @@ -1,6 +1,6 @@ - + diff --git a/view/adminhtml/templates/system/config/status.phtml b/view/adminhtml/templates/system/config/status.phtml index 5a5167c..6a6da43 100644 --- a/view/adminhtml/templates/system/config/status.phtml +++ b/view/adminhtml/templates/system/config/status.phtml @@ -1,6 +1,8 @@ getConfig(); +$accountInfo = $block->getAccountInfo(); +$moduleInfo = $block->getModuleInfo(); ?>
@@ -9,10 +11,25 @@ $config = $block->getConfig();
-

Module version escapeHtml($config['module_version']) ?>

+

+ Module version escapeHtml($moduleInfo['version']) ?> + + + ↻ + escapeHtml(__('update available')) ?> + + + +

- +
@@ -24,24 +41,33 @@ $config = $block->getConfig();

API connection

- accountInfo['name'])): ?> +
Account name
-
escapeHtml($block->accountInfo['name']) ?>
+
escapeHtml($accountInfo['name']) ?>
Subscription status
- + - accountInfo['subscription'])): ?> + accountInfo['subscription']['limit']; - $usage = $block->accountInfo['subscription']['usage']; + $limit = $accountInfo['subscription']['limit']; + $usage = $accountInfo['subscription']['usage']; $usage_percent = round($usage / $limit * 100, 1); $low = round($limit * .75, 2); $high = round($limit * .9, 2); ?> - - euro + + euro
diff --git a/view/adminhtml/web/css/postcode-eu.css b/view/adminhtml/web/css/postcode-eu.css index e532176..4487fab 100644 --- a/view/adminhtml/web/css/postcode-eu.css +++ b/view/adminhtml/web/css/postcode-eu.css @@ -108,7 +108,19 @@ background-color: #666; } -.accont-usage-meter { +.account-usage-meter { width: 100px; margin: 0 5px 0 10px; } + +.update-available-message { + display: inline-block; + padding: 1px 10px 2px 7px; + border-radius: 20px; + background-color: #00759a; + color: white; +} + +.update-available-message a { + color: inherit; +}