Replace generic token-TTL constant with per-platform Platform::defaultTokenTtlSeconds()

The single LONG_LIVED_TOKEN_TTL_SECONDS constant (Meta 60-day) plus a loose
inline 7200 for X made it unclear which networks each value applied to. Express
the fallback TTL as a per-platform match method instead, matching how the enum
already exposes every other per-network value, so the network->value mapping is
visible in one place: X 2h, Instagram/Threads 60d, everyone else null (they
always return expires_in). Behavior is unchanged.
This commit is contained in:
Paulo Castellano 2026-07-03 13:11:36 -03:00
parent ba8068f517
commit c8f875550a
5 changed files with 45 additions and 12 deletions

View file

@ -23,13 +23,6 @@ enum Platform: string
case Telegram = 'telegram';
case Discord = 'discord';
/**
* Fallback lifetime, in seconds, for Meta long-lived tokens (Instagram and
* Threads see extendsAccessTokenOnRefresh) when the provider response
* omits expires_in. Meta issues these tokens for 60 days.
*/
public const LONG_LIVED_TOKEN_TTL_SECONDS = 5184000;
/**
* The social network this platform belongs to. Variants that represent the
* same network (LinkedIn profile vs. company page, Instagram standalone vs.
@ -300,6 +293,28 @@ public static function accessTokenExtendingPlatformValues(): array
));
}
/**
* The token lifetime, in seconds, to assume when the provider's OAuth
* response omits expires_in. Each value is that network's own documented
* default:
*
* - X: a 2-hour access token.
* - Instagram / Threads: Meta's 60-day long-lived token.
*
* Networks that always return expires_in (LinkedIn, TikTok, YouTube,
* Pinterest), whose refresh sets a fixed lifetime directly (Bluesky), or
* whose tokens never expire (Facebook, Mastodon, Telegram, Discord) have no
* fallback here and return null.
*/
public function defaultTokenTtlSeconds(): ?int
{
return match ($this) {
self::X => 7200,
self::Instagram, self::Threads => 5184000,
default => null,
};
}
public function queue(): string
{
return 'social-'.$this->value;

View file

@ -69,7 +69,7 @@ public function callback(Request $request): InertiaResponse
$avatarPath = $socialUser->getAvatar() ? uploadFromUrl($socialUser->getAvatar()) : null;
// Calculate token expiration (long-lived tokens last 60 days)
$expiresIn = $socialUser->expiresIn ?? SocialPlatform::LONG_LIVED_TOKEN_TTL_SECONDS;
$expiresIn = $socialUser->expiresIn ?? $this->platform->defaultTokenTtlSeconds();
$tokenExpiresAt = now()->addSeconds($expiresIn);
$workspace->socialAccounts()->updateOrCreate(

View file

@ -117,7 +117,7 @@ public function callback(Request $request): InertiaResponse
$longLivedData = $longLivedResponse->json();
$longLivedToken = $longLivedData['access_token'] ?? $shortLivedToken;
$expiresIn = $longLivedData['expires_in'] ?? SocialPlatform::LONG_LIVED_TOKEN_TTL_SECONDS;
$expiresIn = $longLivedData['expires_in'] ?? $this->platform->defaultTokenTtlSeconds();
// Fetch user profile
$profileResponse = Http::get(config('trypost.platforms.threads.graph_api')."/{$userId}", [

View file

@ -181,7 +181,7 @@ private function refreshXToken(SocialAccount $account): void
$account->update([
'access_token' => data_get($data, 'access_token'),
'refresh_token' => data_get($data, 'refresh_token', $account->refresh_token),
'token_expires_at' => now()->addSeconds(data_get($data, 'expires_in', 7200)),
'token_expires_at' => now()->addSeconds(data_get($data, 'expires_in', $account->platform->defaultTokenTtlSeconds())),
]);
$account->refresh();
@ -328,7 +328,7 @@ private function refreshThreadsToken(SocialAccount $account): void
$account->update([
'access_token' => $newToken,
'refresh_token' => $newToken,
'token_expires_at' => now()->addSeconds(data_get($data, 'expires_in', Platform::LONG_LIVED_TOKEN_TTL_SECONDS)),
'token_expires_at' => now()->addSeconds(data_get($data, 'expires_in', $account->platform->defaultTokenTtlSeconds())),
]);
$account->refresh();
@ -350,7 +350,7 @@ private function refreshInstagramToken(SocialAccount $account): void
$account->update([
'access_token' => $newToken,
'refresh_token' => $newToken,
'token_expires_at' => now()->addSeconds(data_get($data, 'expires_in', Platform::LONG_LIVED_TOKEN_TTL_SECONDS)),
'token_expires_at' => now()->addSeconds(data_get($data, 'expires_in', $account->platform->defaultTokenTtlSeconds())),
]);
$account->refresh();

View file

@ -85,6 +85,24 @@
expect(Platform::Pinterest->supportsTextOnly())->toBeFalse();
});
test('platform exposes the correct default token TTL fallback', function () {
// X access tokens live 2 hours.
expect(Platform::X->defaultTokenTtlSeconds())->toBe(7200);
// Instagram and Threads use Meta's 60-day long-lived token.
expect(Platform::Instagram->defaultTokenTtlSeconds())->toBe(5184000);
expect(Platform::Threads->defaultTokenTtlSeconds())->toBe(5184000);
// Networks that always return expires_in, set a fixed lifetime directly, or
// never expire have no fallback here.
expect(Platform::LinkedIn->defaultTokenTtlSeconds())->toBeNull();
expect(Platform::TikTok->defaultTokenTtlSeconds())->toBeNull();
expect(Platform::YouTube->defaultTokenTtlSeconds())->toBeNull();
expect(Platform::Pinterest->defaultTokenTtlSeconds())->toBeNull();
expect(Platform::Bluesky->defaultTokenTtlSeconds())->toBeNull();
expect(Platform::Facebook->defaultTokenTtlSeconds())->toBeNull();
});
test('platform is enabled by default', function () {
expect(Platform::LinkedIn->isEnabled())->toBeTrue();
expect(Platform::Instagram->isEnabled())->toBeTrue();