query->resolveRange( $request->query('period'), $request->string('from')->toString() ?: null, $request->string('to')->toString() ?: null, ); $dashboard = null; if ($connection !== null && $connection->isConnected() && filled($connection->account_resource_name)) { $dashboard = $this->query->dashboard( $connection, $range['from'], $range['to'], $range['previous_from'], $range['previous_to'], ); } return Inertia::render('Admin/Adsense/Index', [ 'connection' => $this->connectionPayload($connection), 'configured' => $this->oauth->isConfigured(), 'pendingAccounts' => $request->session()->get(AdsenseOAuthService::SESSION_PENDING_ACCOUNTS_KEY, []), 'recommendedPlacements' => array_values((array) config('adsense.recommended_placement_names', [])), 'range' => [ 'period' => $range['period'], 'from' => $range['from']->toDateString(), 'to' => $range['to']->toDateString(), 'previous_from' => $range['previous_from']->toDateString(), 'previous_to' => $range['previous_to']->toDateString(), ], 'dashboard' => $dashboard, 'routes' => [ 'index' => route('admin.adsense.index'), 'connect' => route('admin.adsense.connect'), 'account' => route('admin.adsense.account'), 'sync' => route('admin.adsense.sync'), 'disconnect' => route('admin.adsense.disconnect'), ], ]); } public function connect(Request $request): RedirectResponse { if (! $this->oauth->isConfigured()) { return redirect() ->route('admin.adsense.index') ->with('error', 'Google AdSense OAuth is not configured.'); } try { return redirect()->away($this->oauth->authorizationUrl($request, true)); } catch (AdsenseOAuthException $exception) { return redirect() ->route('admin.adsense.index') ->with('error', $exception->getMessage()); } } public function callback(Request $request): RedirectResponse { try { $code = $this->oauth->assertValidCallback($request); $tokens = $this->oauth->exchangeAuthorizationCode($code); $connection = AdsenseConnection::current() ?? new AdsenseConnection; $connection->status = AdsenseConnection::STATUS_PENDING; $this->oauth->persistTokens($connection, $tokens, $request->user()?->id); $accounts = $this->api->listAccounts($connection); } catch (AdsenseOAuthException|AdsenseAuthorizationException|AdsenseApiException $exception) { Log::warning('AdSense OAuth callback failed.', AdsenseLogSanitizer::context([ 'message' => $exception->getMessage(), ])); return redirect() ->route('admin.adsense.index') ->with('error', $exception->getMessage()); } $normalized = collect($accounts) ->map(function (array $account): ?array { $name = (string) ($account['name'] ?? ''); if ($name === '') { return null; } return [ 'resource_name' => $name, 'display_name' => (string) ($account['displayName'] ?? $name), ]; }) ->filter() ->values() ->all(); if ($normalized === []) { $connection->status = AdsenseConnection::STATUS_ERROR; $connection->last_error = 'No Google AdSense accounts were available for this Google account.'; $connection->save(); $request->session()->forget(AdsenseOAuthService::SESSION_PENDING_ACCOUNTS_KEY); return redirect() ->route('admin.adsense.index') ->with('error', $connection->last_error); } if (count($normalized) === 1) { $this->assignAccount($connection, $normalized[0]['resource_name'], $normalized[0]['display_name']); $this->queueAdsenseSync($connection, 30); $request->session()->forget(AdsenseOAuthService::SESSION_PENDING_ACCOUNTS_KEY); return redirect() ->route('admin.adsense.index') ->with('success', 'Google AdSense connected. Initial synchronization has started.'); } $request->session()->put(AdsenseOAuthService::SESSION_PENDING_ACCOUNTS_KEY, $normalized); $connection->status = AdsenseConnection::STATUS_PENDING; $connection->save(); return redirect() ->route('admin.adsense.index') ->with('warning', 'Select the Google AdSense account to use for Skinbase analytics.'); } public function selectAccount(Request $request): RedirectResponse { $validated = $request->validate([ 'account_resource_name' => ['required', 'string', 'max:128'], ]); $connection = AdsenseConnection::current(); if ($connection === null || ! $connection->hasRefreshToken()) { return redirect() ->route('admin.adsense.index') ->with('error', 'Connect Google AdSense before selecting an account.'); } $pending = collect($request->session()->get(AdsenseOAuthService::SESSION_PENDING_ACCOUNTS_KEY, [])); $selected = $pending->firstWhere('resource_name', $validated['account_resource_name']); if (! is_array($selected)) { return redirect() ->route('admin.adsense.index') ->with('error', 'The selected AdSense account is not available.'); } $this->assignAccount( $connection, (string) $selected['resource_name'], (string) ($selected['display_name'] ?? $selected['resource_name']), ); $this->queueAdsenseSync($connection, 30); $request->session()->forget(AdsenseOAuthService::SESSION_PENDING_ACCOUNTS_KEY); return redirect() ->route('admin.adsense.index') ->with('success', 'AdSense account selected. Initial synchronization has started.'); } public function sync(Request $request): RedirectResponse { $connection = AdsenseConnection::current(); if ($connection === null || ! $connection->isConnected()) { return redirect() ->route('admin.adsense.index') ->with('error', $connection?->needsReconnect() ? 'AdSense authorization expired or was revoked. Reconnect Google AdSense.' : 'Google AdSense is not connected.'); } $this->queueAdsenseSync($connection, 3); return redirect() ->route('admin.adsense.index') ->with('success', 'AdSense synchronization has been queued.'); } public function disconnect(): RedirectResponse { $connection = AdsenseConnection::current(); if ($connection !== null) { $this->oauth->disconnect($connection); } return redirect() ->route('admin.adsense.index') ->with('success', 'Google AdSense has been disconnected. Historical statistics were kept.'); } /** * @return array|null */ private function connectionPayload(?AdsenseConnection $connection): ?array { if ($connection === null) { return null; } return [ 'status' => $connection->status, 'account_resource_name' => $connection->account_resource_name, 'account_display_name' => $connection->account_display_name, 'connected_at' => optional($connection->connected_at)?->toIso8601String(), 'last_sync_attempt_at' => optional($connection->last_sync_attempt_at)?->toIso8601String(), 'last_successful_sync_at' => optional($connection->last_successful_sync_at)?->toIso8601String(), 'last_error' => $connection->last_error, 'needs_reconnect' => $connection->needsReconnect(), ]; } private function assignAccount(AdsenseConnection $connection, string $resourceName, string $displayName): void { $connection->account_resource_name = $resourceName; $connection->account_display_name = $displayName; $connection->status = AdsenseConnection::STATUS_CONNECTED; $connection->last_error = null; if ($connection->connected_at === null) { $connection->connected_at = now(); } $connection->save(); } private function queueAdsenseSync(AdsenseConnection $connection, int $days): void { $to = now()->startOfDay(); $from = $to->copy()->subDays(max(1, $days) - 1); SyncAdsenseJob::dispatch( (int) $connection->id, $from->toDateString(), $to->toDateString(), ); } }