diff --git a/composer.json b/composer.json index b14d5dce1..8e6beeb57 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,6 @@ "friendsofphp/php-cs-fixer": "^3.86", "graham-campbell/analyzer": "^5.0", "guzzlehttp/guzzle": "^7.7", - "jamesmills/laravel-timezone": "dev-master", "jenssegers/agent": "^2.6", "larastan/larastan": "^3.0", "laravel/fortify": "^1.25", @@ -48,7 +47,8 @@ "spatie/laravel-sluggable": "^3.6", "spatie/pest-plugin-snapshots": "^2.2", "spatie/regex": "^3.1", - "spatie/yaml-front-matter": "^2.0" + "spatie/yaml-front-matter": "^2.0", + "torann/geoip": "^3.0" }, "autoload": { "psr-4": { @@ -71,12 +71,6 @@ ] } }, - "repositories": [ - { - "type": "vcs", - "url": "https://github.com/peoplelikeushomeexchange/laravel-timezone.git" - } - ], "scripts": { "format": [ "vendor/bin/php-cs-fixer fix" diff --git a/composer.lock b/composer.lock index 62ae74079..984ee2161 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "4ce082e86d38b69fa7f8962ea06cf32f", + "content-hash": "6297b47f71add2fd405d902a1fcf4a16", "packages": [ { "name": "bacon/bacon-qr-code", @@ -2350,68 +2350,6 @@ }, "time": "2026-01-28T22:20:33+00:00" }, - { - "name": "jamesmills/laravel-timezone", - "version": "dev-master", - "source": { - "type": "git", - "url": "https://github.com/peoplelikeushomeexchange/laravel-timezone.git", - "reference": "1b3c12f38c71864a99ff0fc3b668f8991d945265" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/peoplelikeushomeexchange/laravel-timezone/zipball/1b3c12f38c71864a99ff0fc3b668f8991d945265", - "reference": "1b3c12f38c71864a99ff0fc3b668f8991d945265", - "shasum": "" - }, - "require": { - "laravel/framework": "5.6.*|5.7.*|5.8.*|^6|^7|^8|^9|^10.0|^11.0|^12.0|^13.0", - "nesbot/carbon": "^1.0|^2.0|^3.0", - "php": ">=7.4", - "torann/geoip": "^3.0" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^2.12|^3.14" - }, - "default-branch": true, - "type": "library", - "extra": { - "laravel": { - "providers": [ - "JamesMills\\LaravelTimezone\\LaravelTimezoneServiceProvider" - ] - } - }, - "autoload": { - "psr-4": { - "JamesMills\\LaravelTimezone\\": "src/", - "JamesMills\\LaravelTimezone\\Database\\Seeds\\": "database/seeds/" - } - }, - "scripts": { - "pre-package-install": [ - "@php artisan config:clear" - ] - }, - "license": [ - "MIT" - ], - "authors": [ - { - "name": "James Mills", - "email": "james@jamesmills.co.uk" - } - ], - "description": "Timezone storage and retrieval for Laravel", - "keywords": [ - "laravel", - "timezone" - ], - "support": { - "source": "https://github.com/peoplelikeushomeexchange/laravel-timezone/tree/master" - }, - "time": "2026-04-04T03:39:37+00:00" - }, { "name": "jaybizzle/crawler-detect", "version": "v1.4.1", @@ -14766,9 +14704,7 @@ ], "aliases": [], "minimum-stability": "dev", - "stability-flags": { - "jamesmills/laravel-timezone": 20 - }, + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { diff --git a/src/Blog/Components/Kiosk/CreateArticle.php b/src/Blog/Components/Kiosk/CreateArticle.php index 21a599441..f535f8c75 100644 --- a/src/Blog/Components/Kiosk/CreateArticle.php +++ b/src/Blog/Components/Kiosk/CreateArticle.php @@ -6,6 +6,7 @@ use ARKEcosystem\Foundation\Blog\Enums\Category; use ARKEcosystem\Foundation\Blog\Models\Article; +use ARKEcosystem\Foundation\Support\Timezone; use ARKEcosystem\Foundation\UserInterface\Components\Concerns\HandleToast; use Illuminate\Contracts\Support\Renderable; use Illuminate\Http\UploadedFile; @@ -13,7 +14,6 @@ use Illuminate\Support\Str; use Illuminate\Validation\Rule; use Illuminate\Validation\Rules\Enum; -use JamesMills\LaravelTimezone\Facades\Timezone; use Livewire\Component; use Livewire\WithFileUploads; @@ -48,7 +48,7 @@ public function save() : void ]); if ($this->state['published_at'] !== '' && $this->state['published_at'] !== null) { - $this->state['published_at'] = Timezone::convertFromLocal($this->state['published_at']); + $this->state['published_at'] = Timezone::fromLocal($this->state['published_at']); } else { $this->state['published_at'] = null; } diff --git a/src/Blog/Components/Kiosk/UpdateArticle.php b/src/Blog/Components/Kiosk/UpdateArticle.php index f95be2f19..0ad8dec1a 100644 --- a/src/Blog/Components/Kiosk/UpdateArticle.php +++ b/src/Blog/Components/Kiosk/UpdateArticle.php @@ -7,6 +7,7 @@ use ARKEcosystem\Foundation\Blog\Enums\Category; use ARKEcosystem\Foundation\Blog\Models\Article; use ARKEcosystem\Foundation\Blog\Models\User; +use ARKEcosystem\Foundation\Support\Timezone; use ARKEcosystem\Foundation\UserInterface\Components\Concerns\HandleToast; use Illuminate\Contracts\Support\Renderable; use Illuminate\Database\Eloquent\Collection; @@ -15,7 +16,6 @@ use Illuminate\Support\Str; use Illuminate\Validation\Rule; use Illuminate\Validation\Rules\Enum; -use JamesMills\LaravelTimezone\Facades\Timezone; use Livewire\Component; use Livewire\WithFileUploads; @@ -53,8 +53,7 @@ public function mount(Article $article) : void $this->users = User::orderBy('name')->withTrashed()->get(); if ($article->published_at !== null) { - // @phpstan-ignore-next-line - $this->state['published_at'] = Timezone::convertToLocal($article->published_at, 'Y-m-d\TH:i'); + $this->state['published_at'] = Timezone::toLocal($article->published_at)->format('Y-m-d\TH:i'); } } @@ -70,7 +69,7 @@ public function save() : void ]); if ($this->state['published_at'] !== '' && $this->state['published_at'] !== null) { - $this->state['published_at'] = Timezone::convertFromLocal($this->state['published_at']); + $this->state['published_at'] = Timezone::fromLocal($this->state['published_at']); } else { $this->state['published_at'] = null; } diff --git a/src/Blog/Listeners/UpdateUserTimezone.php b/src/Blog/Listeners/UpdateUserTimezone.php new file mode 100644 index 000000000..81568f839 --- /dev/null +++ b/src/Blog/Listeners/UpdateUserTimezone.php @@ -0,0 +1,21 @@ +getLocation(request()->ip())->timezone; + + if ($timezone === null || $event->user->timezone === $timezone) { + return; + } + + $event->user->forceFill(['timezone' => $timezone])->save(); + } +} diff --git a/src/Providers/BlogServiceProvider.php b/src/Providers/BlogServiceProvider.php index 1e9ae976f..e18648ee2 100644 --- a/src/Providers/BlogServiceProvider.php +++ b/src/Providers/BlogServiceProvider.php @@ -17,6 +17,9 @@ use ARKEcosystem\Foundation\Blog\Controllers\Contracts\ArticleController as ArticleControllerContract; use ARKEcosystem\Foundation\Blog\Controllers\KioskController; use ARKEcosystem\Foundation\Blog\Controllers\UserController; +use ARKEcosystem\Foundation\Blog\Listeners\UpdateUserTimezone; +use Illuminate\Auth\Events\Login; +use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Route; use Illuminate\Support\ServiceProvider; use Illuminate\View\Compilers\BladeCompiler; @@ -35,6 +38,8 @@ public function boot(): void $this->registerLivewireComponents(); $this->registerRoutes(); + + $this->registerListeners(); } protected function registerContracts(): void @@ -105,4 +110,9 @@ private function registerRoutes(): void }); }); } + + private function registerListeners(): void + { + Event::listen(Login::class, UpdateUserTimezone::class); + } } diff --git a/src/Support/Timezone.php b/src/Support/Timezone.php index 7f90c7abe..f0473b6cd 100644 --- a/src/Support/Timezone.php +++ b/src/Support/Timezone.php @@ -7,7 +7,9 @@ use Carbon\Carbon; use Carbon\CarbonTimeZone; use DateTimeZone; +use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Facades\Config; final class Timezone { @@ -28,4 +30,19 @@ public static function formattedList(): array ]; })->sortBy('offset')->toArray()); } + + public static function toLocal(Carbon $date): Carbon + { + return $date->copy()->setTimezone(static::current()); + } + + public static function fromLocal(string $date): Carbon + { + return Carbon::parse($date, static::current())->setTimezone('UTC'); + } + + private static function current(): string + { + return Auth::user()?->timezone ?? Config::get('app.timezone'); + } } diff --git a/tests/Blog/TestCase.php b/tests/Blog/TestCase.php index 50774cf2f..640542516 100644 --- a/tests/Blog/TestCase.php +++ b/tests/Blog/TestCase.php @@ -11,7 +11,6 @@ use ARKEcosystem\Foundation\Providers\UserInterfaceServiceProvider; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\View; -use JamesMills\LaravelTimezone\LaravelTimezoneServiceProvider; use Livewire\LivewireServiceProvider; use Spatie\MediaLibrary\MediaLibraryServiceProvider; use Spatie\ResponseCache\Middlewares\DoNotCacheResponse; @@ -57,7 +56,6 @@ protected function getPackageProviders($app) LivewireServiceProvider::class, MediaLibraryServiceProvider::class, ResponseCacheServiceProvider::class, - LaravelTimezoneServiceProvider::class, ]; } diff --git a/tests/Blog/Unit/Listeners/UpdateUserTimezoneTest.php b/tests/Blog/Unit/Listeners/UpdateUserTimezoneTest.php new file mode 100644 index 000000000..248a699d3 --- /dev/null +++ b/tests/Blog/Unit/Listeners/UpdateUserTimezoneTest.php @@ -0,0 +1,43 @@ +shouldReceive('getLocation')->andReturn(new Location(['timezone' => $timezone])); + + app()->instance('geoip', $geoip); +} + +it('is registered for the login event', function () { + expect(Event::hasListeners(Login::class))->toBeTrue(); + expect(Event::getRawListeners()[Login::class])->toContain(UpdateUserTimezone::class); +}); + +it('updates the user timezone on login', function () { + fakeGeoIpTimezone('Europe/Ljubljana'); + + $user = User::factory()->create(['timezone' => 'UTC']); + + (new UpdateUserTimezone())->handle(new Login('web', $user, false)); + + expect($user->fresh()->timezone)->toBe('Europe/Ljubljana'); +}); + +it('does not touch the user when the timezone could not be resolved', function () { + fakeGeoIpTimezone(null); + + $user = User::factory()->create(['timezone' => 'America/Mexico_City']); + + (new UpdateUserTimezone())->handle(new Login('web', $user, false)); + + expect($user->fresh()->timezone)->toBe('America/Mexico_City'); +}); diff --git a/tests/Blog/Unit/TimezoneTest.php b/tests/Blog/Unit/TimezoneTest.php new file mode 100644 index 000000000..f867afc3c --- /dev/null +++ b/tests/Blog/Unit/TimezoneTest.php @@ -0,0 +1,31 @@ +actingAs(User::factory()->create(['timezone' => 'America/Mexico_City'])); + + $date = Carbon::parse('2026-01-01 12:00:00', 'UTC'); + + expect(Timezone::toLocal($date)->format('Y-m-d H:i'))->toBe('2026-01-01 06:00'); + expect($date->timezoneName)->toBe('UTC'); +}); + +it('converts a local date to utc', function () { + $this->actingAs(User::factory()->create(['timezone' => 'America/Mexico_City'])); + + $date = Timezone::fromLocal('2026-01-01T06:00'); + + expect($date->timezoneName)->toBe('UTC'); + expect($date->format('Y-m-d H:i'))->toBe('2026-01-01 12:00'); +}); + +it('falls back to the app timezone for guests', function () { + config(['app.timezone' => 'UTC']); + + expect(Timezone::fromLocal('2026-01-01T06:00')->format('Y-m-d H:i'))->toBe('2026-01-01 06:00'); +});