Skip to content

Commit

Permalink
fix: Improved how we check for banner dismissals
Browse files Browse the repository at this point in the history
  • Loading branch information
lewislarsen committed Jul 30, 2024
1 parent 340b979 commit 95fb1e8
Show file tree
Hide file tree
Showing 4 changed files with 351 additions and 68 deletions.
138 changes: 100 additions & 38 deletions app/Console/Commands/FetchNewFeatures.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,51 +6,86 @@

use Exception;
use Illuminate\Console\Command;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Http\Client\RequestException;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Symfony\Component\Console\Command\Command as CommandAlias;

/**
* Command to fetch new features from GitHub or a local file and store them in cache.
* Fetches new features from GitHub or a local file and stores them in cache.
*
* This command retrieves feature information either from a remote GitHub repository
* or a local JSON file, validates the data, and stores the latest valid feature
* in the application's cache for a week.
*/
class FetchNewFeatures extends Command
{
/**
* The console command signature.
*
* @var string
*/
protected $signature = 'vanguard:fetch-new-features {--local : Fetch from local new_features.json file}';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Fetch new features from GitHub or local file and store in cache';

/**
* Handle the command execution.
* Execute the console command.
*
* This method orchestrates the feature fetching process, including fetching,
* validating, and storing the latest feature if it's new.
*
* @return int The command exit code (0 for success, 1 for failure)
*/
public function handle(): int
{
try {
$features = $this->fetchFeatures();

if ($features === []) {
$this->error('No new features found.');
$this->components->error('No new features found.');

return CommandAlias::SUCCESS;
}

return CommandAlias::FAILURE;
$latestFeature = $this->getLatestValidFeature($features);

if ($latestFeature === null) {
$this->components->error('No valid features found.');

return CommandAlias::SUCCESS;
}

if (! $this->isNewFeature($latestFeature)) {
$this->components->error('No new features to store.');

return CommandAlias::SUCCESS;
}

$this->storeLatestFeature($features);
$this->info('New features fetched and stored successfully.');
$this->storeLatestFeature($latestFeature);
$this->components->info('New features fetched and stored successfully.');

return CommandAlias::SUCCESS;
} catch (Exception $e) {
$this->error($e->getMessage());
Log::error('Failed to fetch new features: ' . $e->getMessage());

return CommandAlias::FAILURE;
}
}

/**
* Fetch features from either local or remote source.
* Fetch features from either remote or local source based on the --local option.
*
* @return array<int, array<string, string>>
* @return array<int, array<string, string>> An array of feature data
*
* @throws Exception If fetching or parsing fails
*/
private function fetchFeatures(): array
{
Expand All @@ -60,28 +95,27 @@ private function fetchFeatures(): array
/**
* Fetch features from the remote GitHub repository.
*
* @return array<int, array<string, string>>
* @return array<int, array<string, string>> An array of feature data
*
* @throws Exception
* @throws Exception If the HTTP request fails or returns an unsuccessful status
*/
private function fetchRemote(): array
{
try {
$response = Http::get('https://raw.githubusercontent.com/vanguardbackup/vanguard/main/new_features.json');
$response->throw();
$response = Http::get('https://raw.githubusercontent.com/vanguardbackup/vanguard/main/new_features.json');

return $response->json();
} catch (RequestException $e) {
throw new Exception("Failed to fetch new features from remote: {$e->getMessage()}", $e->getCode(), $e);
if (! $response->successful()) {
throw new Exception("Failed to fetch new features from remote: HTTP request returned status code {$response->status()}");
}

return $response->json();
}

/**
* Fetch features from the local file.
* Fetch features from the local new_features.json file.
*
* @return array<int, array<string, string>>
* @return array<int, array<string, string>> An array of feature data
*
* @throws Exception
* @throws Exception If the file is not found or cannot be parsed
*/
private function fetchLocal(): array
{
Expand All @@ -102,37 +136,65 @@ private function fetchLocal(): array
}

/**
* Store the latest feature in the cache if available.
* Get the latest valid feature from the fetched features.
*
* @param array<int, array<string, string>> $features
*
* @throws FileNotFoundException
* @param array<int, array<string, string>> $features The array of fetched features
* @return array<string, string>|null The latest valid feature or null if none found
*/
private function storeLatestFeature(array $features): void
private function getLatestValidFeature(array $features): ?array
{
if ($features === []) {
$this->info('No features to store.');
$validFeatures = array_filter($features, [$this, 'isValidFeature']);

return;
}
return end($validFeatures) ?: null;
}

$latestFeature = end($features);
if ($latestFeature === false) {
$this->error('Failed to retrieve the latest feature.');
/**
* Check if a feature is valid (has all required fields).
*
* @param array<string, string> $feature The feature to validate
* @return bool True if the feature is valid, false otherwise
*/
private function isValidFeature(array $feature): bool
{
return isset($feature['title'], $feature['description'], $feature['version']);
}

return;
}
/**
* Check if the given feature is newer than the currently cached feature.
*
* @param array<string, string> $feature The feature to check
* @return bool True if the feature is new, false otherwise
*/
private function isNewFeature(array $feature): bool
{
$cachedFeature = Cache::get('latest_feature');

return ! $cachedFeature || version_compare($feature['version'], $cachedFeature['version'], '>');
}

/**
* Store the latest feature in the cache.
*
* This method adds the current application version to the feature data
* and ensures a GitHub URL is present before caching.
*
* @param array<string, string> $latestFeature The feature to store
*/
private function storeLatestFeature(array $latestFeature): void
{
$latestFeature['current_version'] = $this->getCurrentVersion();
$latestFeature['github_url'] ??= 'https://github.com/vanguardbackup/vanguard';

Cache::put('latest_feature', $latestFeature, now()->addDay());
Cache::put('latest_feature', $latestFeature, now()->addWeek());
}

/**
* Get the current version from the VERSION file.
* Get the current version of the application.
*
* This method reads the version from a VERSION file in the project root.
* If the file doesn't exist, it returns '0.0.0' as a default version.
*
* @throws FileNotFoundException
* @return string The current version or '0.0.0' if not found
*/
private function getCurrentVersion(): string
{
Expand Down
66 changes: 54 additions & 12 deletions app/Livewire/Other/NewFeatureBanner.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,51 +6,93 @@

use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Session;
use Illuminate\View\View;
use Livewire\Component;

/**
* Displays a banner for new features in the application.
* Fetches the latest feature from cache and allows users to dismiss it.
*
* This Livewire component fetches the latest feature from cache,
* displays it to the user, and allows them to dismiss it. The dismissal
* state is stored in the user's session to prevent the same feature
* from being shown repeatedly.
*/
class NewFeatureBanner extends Component
{
/**
* The latest feature to display in the banner.
* The session key used to store the dismissed feature version.
*
* @var string
*/
private const string SESSION_KEY = 'dismissed_feature_version';
/**
* The latest feature to be displayed in the banner.
*
* @var array<string, string>|null
*/
public ?array $latestFeature = null;

/**
* Initialize the component state.
*
* This method is called when the component is first loaded.
* It loads the latest feature from cache if available and not dismissed.
*/
public function mount(): void
{
$cachedFeature = Cache::get('latest_feature');

if (is_array($cachedFeature)) {
$this->latestFeature = $cachedFeature;
} elseif ($cachedFeature !== null) {
Log::warning('Unexpected data type for latest_feature in cache', ['type' => gettype($cachedFeature)]);
$this->latestFeature = null;
}
$this->loadLatestFeature();
}

/**
* Render the component.
*
* @return View The view that represents the component
*/
public function render(): View
{
return view('livewire.other.new-feature-banner');
}

/**
* Dismiss the feature banner.
* Dismiss the currently displayed feature.
*
* This method is called when the user chooses to dismiss the feature banner.
* It stores the dismissed version in the session and clears the latestFeature property.
*/
public function dismiss(): void
{
$this->latestFeature = null;
if ($this->latestFeature) {
Session::put(self::SESSION_KEY, $this->latestFeature['version'] ?? 'unknown');
$this->latestFeature = null;
}
$this->dispatch('featureDismissed');
}

/**
* Load the latest feature from cache if it hasn't been dismissed.
*
* This method checks the cache for the latest feature and compares it
* against the dismissed version stored in the session. If the feature
* is new or hasn't been dismissed, it's loaded into the component state.
*/
private function loadLatestFeature(): void
{
$cachedFeature = Cache::get('latest_feature');

if (! is_array($cachedFeature)) {
if ($cachedFeature !== null) {
Log::warning('Unexpected data type for latest_feature in cache', ['type' => gettype($cachedFeature)]);
}

return;
}

$dismissedVersion = Session::get(self::SESSION_KEY);
$currentVersion = $cachedFeature['version'] ?? 'unknown';

if ($dismissedVersion !== $currentVersion) {
$this->latestFeature = $cachedFeature;
}
}
}
Loading

0 comments on commit 95fb1e8

Please sign in to comment.