|null */ public ?array $existingAccount = null; // Feed form public string $feedName = ''; public string $feedProvider = 'vrt'; public ?int $feedLanguageId = null; public string $feedDescription = ''; // Channel form public string $channelName = ''; public ?int $platformInstanceId = null; public ?int $channelLanguageId = null; public string $channelDescription = ''; // Route form public ?int $routeFeedId = null; public ?int $routeChannelId = null; public int $routePriority = 50; // State /** @var array */ public array $formErrors = []; public bool $isLoading = false; #[\Livewire\Attributes\Locked] public ?int $previousChannelLanguageId = null; protected CreatePlatformAccountAction $createPlatformAccountAction; protected CreateFeedAction $createFeedAction; protected CreateChannelAction $createChannelAction; protected CreateRouteAction $createRouteAction; public function boot( CreatePlatformAccountAction $createPlatformAccountAction, CreateFeedAction $createFeedAction, CreateChannelAction $createChannelAction, CreateRouteAction $createRouteAction, ): void { $this->createPlatformAccountAction = $createPlatformAccountAction; $this->createFeedAction = $createFeedAction; $this->createChannelAction = $createChannelAction; $this->createRouteAction = $createRouteAction; } public function mount(): void { // Check for existing platform account $account = PlatformAccount::where('is_active', true)->first(); if ($account) { $this->existingAccount = [ 'id' => $account->id, 'username' => $account->username, 'instance_url' => $account->instance_url, ]; } // Pre-fill feed form if exists $feed = Feed::where('is_active', true)->first(); if ($feed) { $this->feedName = $feed->name; $this->feedProvider = $feed->provider ?? 'vrt'; $this->feedLanguageId = $feed->language_id; $this->feedDescription = $feed->description ?? ''; } // Pre-fill channel form if exists $channel = PlatformChannel::where('is_active', true)->first(); if ($channel) { $this->channelName = $channel->name; $this->platformInstanceId = $channel->platform_instance_id; $this->channelLanguageId = $channel->language_id; $this->channelDescription = $channel->description ?? ''; } // Pre-fill route form if exists $route = Route::where('is_active', true)->first(); if ($route) { $this->routeFeedId = $route->feed_id; $this->routeChannelId = $route->platform_channel_id; $this->routePriority = $route->priority; } } public function goToStep(int $step): void { $this->step = $step; $this->formErrors = []; } public function nextStep(): void { $this->step++; $this->formErrors = []; // When entering feed step, inherit language from channel if ($this->step === 4 && $this->channelLanguageId) { $this->feedLanguageId = $this->channelLanguageId; } } public function previousStep(): void { if ($this->step > 1) { $this->step--; $this->formErrors = []; } } public function continueWithExistingAccount(): void { $this->nextStep(); } public function deleteAccount(): void { if ($this->existingAccount) { PlatformAccount::destroy($this->existingAccount['id']); $this->existingAccount = null; } } public function createPlatformAccount(): void { $this->formErrors = []; $this->isLoading = true; $this->validate([ 'instanceUrl' => 'required|string|max:255|regex:/^[a-zA-Z0-9]([a-zA-Z0-9\-\.]*[a-zA-Z0-9])?$/', 'username' => 'required|string|max:255', 'password' => 'required|string|min:6', ], [ 'instanceUrl.regex' => 'Please enter a valid domain name (e.g., lemmy.world, belgae.social)', ]); try { $platformAccount = $this->createPlatformAccountAction->execute( $this->instanceUrl, $this->username, $this->password, ); $this->existingAccount = [ 'id' => $platformAccount->id, 'username' => $platformAccount->username, 'instance_url' => $platformAccount->instance_url, ]; $this->nextStep(); } catch (PlatformAuthException $e) { $message = $e->getMessage(); if (str_contains($message, 'Rate limited by')) { $this->formErrors['general'] = $message; } elseif (str_contains($message, 'Connection failed')) { $this->formErrors['general'] = 'Unable to connect to the Lemmy instance. Please check the URL and try again.'; } else { $this->formErrors['general'] = 'Invalid username or password. Please check your credentials and try again.'; } } catch (Exception $e) { logger()->error('Lemmy platform account creation failed', [ 'instance_url' => 'https://'.$this->instanceUrl, 'username' => $this->username, 'error' => $e->getMessage(), 'class' => get_class($e), ]); $this->formErrors['general'] = 'An error occurred while setting up your account. Please try again.'; } finally { $this->isLoading = false; } } public function createFeed(): void { $this->formErrors = []; $this->isLoading = true; // Get available provider codes for validation $availableProviders = collect($this->getProvidersForLanguage())->pluck('code')->implode(','); $this->validate([ 'feedName' => 'required|string|max:255', 'feedProvider' => "required|in:{$availableProviders}", 'feedLanguageId' => 'required|exists:languages,id', 'feedDescription' => 'nullable|string|max:1000', ]); try { $this->createFeedAction->execute( $this->feedName, $this->feedProvider, $this->feedLanguageId, $this->feedDescription ?: null, ); $this->nextStep(); } catch (InvalidArgumentException $e) { $this->formErrors['general'] = 'Invalid provider and language combination.'; } catch (Exception $e) { $this->formErrors['general'] = 'Failed to create feed. Please try again.'; } finally { $this->isLoading = false; } } public function createChannel(): void { $this->formErrors = []; $this->isLoading = true; $this->validate([ 'channelName' => 'required|string|max:255', 'platformInstanceId' => 'required|exists:platform_instances,id', 'channelLanguageId' => 'required|exists:languages,id', 'channelDescription' => 'nullable|string|max:1000', ]); // If language changed, reset feed form if ($this->previousChannelLanguageId !== null && $this->previousChannelLanguageId !== $this->channelLanguageId) { $this->feedName = ''; $this->feedProvider = ''; $this->feedDescription = ''; $this->routeFeedId = null; $this->routeChannelId = null; } $this->previousChannelLanguageId = $this->channelLanguageId; try { $channel = $this->createChannelAction->execute( $this->channelName, $this->platformInstanceId, $this->channelLanguageId, $this->channelDescription ?: null, ); // Sync existing posts from this channel for duplicate detection SyncChannelPostsJob::dispatch($channel); $this->nextStep(); } catch (RuntimeException $e) { $this->formErrors['general'] = $e->getMessage(); } catch (Exception $e) { $this->formErrors['general'] = 'Failed to create channel. Please try again.'; } finally { $this->isLoading = false; } } public function createRoute(): void { $this->formErrors = []; $this->isLoading = true; $this->validate([ 'routeFeedId' => 'required|exists:feeds,id', 'routeChannelId' => 'required|exists:platform_channels,id', 'routePriority' => 'nullable|integer|min:1|max:100', ]); try { $this->createRouteAction->execute( $this->routeFeedId, $this->routeChannelId, $this->routePriority, ); // Trigger article discovery ArticleDiscoveryJob::dispatch(); $this->nextStep(); } catch (Exception $e) { $this->formErrors['general'] = 'Failed to create route. Please try again.'; } finally { $this->isLoading = false; } } public function completeOnboarding(): void { Setting::updateOrCreate( ['key' => 'onboarding_completed'], ['value' => now()->toIso8601String()] ); app(OnboardingService::class)->clearCache(); $this->redirect(route('dashboard')); } /** * Get language codes that have at least one active provider. */ /** * @return list */ public function getAvailableLanguageCodes(): array { $providers = config('feed.providers', []); $languageCodes = []; foreach ($providers as $provider) { if (! ($provider['is_active'] ?? false)) { continue; } foreach (array_keys($provider['languages'] ?? []) as $code) { $languageCodes[(string) $code] = true; } } return array_keys($languageCodes); } /** * Get providers available for the current channel language. */ /** * @return array> */ public function getProvidersForLanguage(): array { if (! $this->channelLanguageId) { return []; } $language = Language::find($this->channelLanguageId); if (! $language) { return []; } $langCode = $language->short_code; $providers = config('feed.providers', []); $available = []; foreach ($providers as $key => $provider) { if (! ($provider['is_active'] ?? false)) { continue; } if (isset($provider['languages'][$langCode])) { $available[] = [ 'code' => $provider['code'], 'name' => $provider['name'], 'description' => $provider['description'] ?? '', ]; } } return $available; } /** * Get the current channel language model. */ public function getChannelLanguage(): ?Language { if (! $this->channelLanguageId) { return null; } return Language::find($this->channelLanguageId); } public function render(): \Illuminate\Contracts\View\View { // For channel step: only show languages that have providers $availableCodes = $this->getAvailableLanguageCodes(); $wizardLanguages = Language::where('is_active', true) ->whereIn('short_code', $availableCodes) ->orderBy('name') ->get(); $platformInstances = PlatformInstance::where('is_active', true)->orderBy('name')->get(); $feeds = Feed::with('language')->where('is_active', true)->orderBy('name')->get(); $channels = PlatformChannel::with('language')->where('is_active', true)->orderBy('name')->get(); // For feed step: only show providers for the channel's language $feedProviders = collect($this->getProvidersForLanguage()); // Get channel language for display $channelLanguage = $this->getChannelLanguage(); return view('livewire.onboarding', [ 'wizardLanguages' => $wizardLanguages, 'platformInstances' => $platformInstances, 'feeds' => $feeds, 'channels' => $channels, 'feedProviders' => $feedProviders, 'channelLanguage' => $channelLanguage, ])->layout('layouts.onboarding'); } }