From 5a0022af72ea6cd9ae7e91952ffb392cba40540f Mon Sep 17 00:00:00 2001 From: isaacclad Date: Wed, 8 Jul 2026 07:10:06 +0000 Subject: [PATCH] Add Integrations hub for Woo marketing and shipping extensions. Surface Google, Pinterest, Snapchat, and shipping status from the WordPress plugin with deep links to finish setup in WooCommerce. Co-authored-by: Cursor --- .../Controllers/Woo/IntegrationController.php | 28 +++++ app/Services/Woo/IntegrationStatusService.php | 114 ++++++++++++++++++ app/Support/MobileTopbar.php | 1 + config/woo_integrations.php | 32 +++++ resources/views/partials/sidebar.blade.php | 2 + .../views/woo/integrations/index.blade.php | 91 ++++++++++++++ resources/views/woo/settings.blade.php | 9 ++ routes/web.php | 2 + tests/Feature/WooIntegrationsTest.php | 101 ++++++++++++++++ tests/Feature/WooMobileNavTest.php | 8 +- 10 files changed, 384 insertions(+), 4 deletions(-) create mode 100644 app/Http/Controllers/Woo/IntegrationController.php create mode 100644 app/Services/Woo/IntegrationStatusService.php create mode 100644 config/woo_integrations.php create mode 100644 resources/views/woo/integrations/index.blade.php create mode 100644 tests/Feature/WooIntegrationsTest.php diff --git a/app/Http/Controllers/Woo/IntegrationController.php b/app/Http/Controllers/Woo/IntegrationController.php new file mode 100644 index 0000000..67d142d --- /dev/null +++ b/app/Http/Controllers/Woo/IntegrationController.php @@ -0,0 +1,28 @@ +currentStore($request); + $payload = $this->integrations->forStore($store); + + return view('woo.integrations.index', [ + 'store' => $store, + 'reachable' => $payload['reachable'], + 'integrations' => $payload['integrations'], + ]); + } +} diff --git a/app/Services/Woo/IntegrationStatusService.php b/app/Services/Woo/IntegrationStatusService.php new file mode 100644 index 0000000..22e93cb --- /dev/null +++ b/app/Services/Woo/IntegrationStatusService.php @@ -0,0 +1,114 @@ +> + * } + */ + public function forStore(?WooStore $store): array + { + $catalog = config('woo_integrations', []); + + if (! $store) { + return [ + 'store' => null, + 'reachable' => false, + 'integrations' => $this->catalogOnly($catalog), + ]; + } + + $remote = $this->client->get($store, 'integrations/status'); + $reachable = is_array($remote); + + if (! $reachable) { + Log::info('Woo integration status unavailable', ['store_id' => $store->id]); + } + + $remoteItems = is_array($remote['integrations'] ?? null) ? $remote['integrations'] : []; + + $integrations = []; + foreach ($catalog as $key => $meta) { + $remoteStatus = is_array($remoteItems[$key] ?? null) ? $remoteItems[$key] : []; + $integrations[] = $this->mergeIntegration($key, $meta, $remoteStatus, $store, $reachable); + } + + return [ + 'store' => $store, + 'reachable' => $reachable, + 'integrations' => $integrations, + ]; + } + + /** + * @param array $meta + * @param array $remoteStatus + * @return array + */ + private function mergeIntegration(string $key, array $meta, array $remoteStatus, WooStore $store, bool $reachable): array + { + $state = $reachable + ? (string) ($remoteStatus['state'] ?? 'unknown') + : 'unavailable'; + + $manageUrl = $remoteStatus['manage_url'] ?? null; + if (is_string($manageUrl) && $manageUrl !== '' && ! str_starts_with($manageUrl, 'http')) { + $manageUrl = rtrim((string) $store->site_url, '/').'/'.ltrim($manageUrl, '/'); + } + + return [ + 'key' => $key, + 'name' => (string) ($meta['name'] ?? $key), + 'description' => (string) ($meta['description'] ?? ''), + 'category' => (string) ($meta['category'] ?? 'Integration'), + 'install_url' => $meta['install_url'] ?? null, + 'docs_url' => $meta['docs_url'] ?? null, + 'state' => $state, + 'installed' => (bool) ($remoteStatus['installed'] ?? false), + 'connected' => (bool) ($remoteStatus['connected'] ?? false), + 'summary' => $reachable + ? (string) ($remoteStatus['summary'] ?? '') + : 'Could not reach your store. Check that the Ladill plugin is active.', + 'manage_url' => is_string($manageUrl) && $manageUrl !== '' ? $manageUrl : null, + 'details' => is_array($remoteStatus['details'] ?? null) ? $remoteStatus['details'] : [], + ]; + } + + /** + * @param array> $catalog + * @return list> + */ + private function catalogOnly(array $catalog): array + { + $integrations = []; + + foreach ($catalog as $key => $meta) { + $integrations[] = [ + 'key' => $key, + 'name' => (string) ($meta['name'] ?? $key), + 'description' => (string) ($meta['description'] ?? ''), + 'category' => (string) ($meta['category'] ?? 'Integration'), + 'install_url' => $meta['install_url'] ?? null, + 'docs_url' => $meta['docs_url'] ?? null, + 'state' => 'no_store', + 'installed' => false, + 'connected' => false, + 'summary' => 'Connect a WooCommerce store to see status.', + 'manage_url' => null, + 'details' => [], + ]; + } + + return $integrations; + } +} diff --git a/app/Support/MobileTopbar.php b/app/Support/MobileTopbar.php index 154c743..4c11e6b 100644 --- a/app/Support/MobileTopbar.php +++ b/app/Support/MobileTopbar.php @@ -16,6 +16,7 @@ class MobileTopbar 'woo.categories.create' => 'New category', 'woo.categories.edit' => 'Edit category', 'woo.categories.*' => 'Categories', + 'woo.integrations.*' => 'Integrations', 'woo.stores.*' => 'Stores', 'woo.settings' => 'Settings', 'woo.pro.*' => 'Plans', diff --git a/config/woo_integrations.php b/config/woo_integrations.php new file mode 100644 index 0000000..6d53528 --- /dev/null +++ b/config/woo_integrations.php @@ -0,0 +1,32 @@ + [ + 'name' => 'Shipping', + 'description' => 'Shipping zones, rates, and WooCommerce Shipping labels from your store.', + 'category' => 'Fulfillment', + 'install_url' => null, + 'docs_url' => 'https://woocommerce.com/document/setting-up-shipping-zones/', + ], + 'google' => [ + 'name' => 'Google for WooCommerce', + 'description' => 'List products on Google and run Performance Max campaigns from WooCommerce.', + 'category' => 'Marketing', + 'install_url' => 'https://wordpress.org/plugins/google-listings-and-ads/', + 'docs_url' => 'https://woocommerce.com/document/google-for-woocommerce/', + ], + 'pinterest' => [ + 'name' => 'Pinterest for WooCommerce', + 'description' => 'Sync your catalog and run Pinterest ads to reach shoppers.', + 'category' => 'Marketing', + 'install_url' => 'https://wordpress.org/plugins/pinterest-for-woocommerce/', + 'docs_url' => 'https://woocommerce.com/document/pinterest-for-woocommerce/', + ], + 'snapchat' => [ + 'name' => 'Snapchat for WooCommerce', + 'description' => 'Connect Snapchat ads, catalog sync, and conversion tracking.', + 'category' => 'Marketing', + 'install_url' => 'https://wordpress.org/plugins/snapchat-for-woocommerce/', + 'docs_url' => 'https://businesshelp.snapchat.com/s/article/woocommerce', + ], +]; diff --git a/resources/views/partials/sidebar.blade.php b/resources/views/partials/sidebar.blade.php index 6cffb6c..93694b2 100644 --- a/resources/views/partials/sidebar.blade.php +++ b/resources/views/partials/sidebar.blade.php @@ -14,6 +14,8 @@ 'icon' => ''], ['name' => 'Categories', 'route' => route('woo.categories.index'), 'active' => request()->routeIs('woo.categories.*'), 'icon' => ''], + ['name' => 'Integrations', 'route' => route('woo.integrations.index'), 'active' => request()->routeIs('woo.integrations.*'), + 'icon' => ''], ['name' => 'Stores', 'route' => route('woo.stores.index'), 'active' => request()->routeIs('woo.stores.*'), 'icon' => ''], ]; diff --git a/resources/views/woo/integrations/index.blade.php b/resources/views/woo/integrations/index.blade.php new file mode 100644 index 0000000..758c4e2 --- /dev/null +++ b/resources/views/woo/integrations/index.blade.php @@ -0,0 +1,91 @@ + + Integrations + +
+
+
+

Integrations

+

+ @if($store) + Marketing and shipping extensions for {{ $store->site_name ?? $store->site_url }}. + @else + Connect a store to see which WooCommerce extensions are installed and configured. + @endif +

+
+ @if($store && ! $reachable) + + Store unreachable — update the Ladill plugin on WordPress + + @endif +
+ + @if(! $store) +
+

No active store selected.

+ + Connect a WooCommerce store + + +
+ @endif + +
+ @foreach($integrations as $integration) + @php + $state = $integration['state']; + $badge = match ($state) { + 'connected' => ['Connected', 'bg-emerald-50 text-emerald-700 ring-emerald-200'], + 'available', 'needs_setup' => $integration['installed'] + ? ['Needs setup', 'bg-amber-50 text-amber-800 ring-amber-200'] + : ['Not installed', 'bg-slate-100 text-slate-600 ring-slate-200'], + 'not_installed' => ['Not installed', 'bg-slate-100 text-slate-600 ring-slate-200'], + 'no_store' => ['No store', 'bg-slate-100 text-slate-600 ring-slate-200'], + default => ['Unavailable', 'bg-rose-50 text-rose-700 ring-rose-200'], + }; + @endphp +
+
+
+

{{ $integration['category'] }}

+

{{ $integration['name'] }}

+
+ {{ $badge[0] }} +
+ +

{{ $integration['description'] }}

+ + @if($integration['summary']) +

{{ $integration['summary'] }}

+ @endif + +
+ @if($integration['manage_url']) + + Manage in WordPress + + @elseif($integration['install_url'] && in_array($state, ['not_installed', 'no_store', 'unavailable'], true)) + + Get extension + + @endif + + @if($integration['docs_url']) + + Docs + + @endif +
+
+ @endforeach +
+ +
+ Ladill surfaces official WooCommerce extensions installed on your store. OAuth, catalog sync, and ad setup still happen in WordPress — open Manage in WordPress to finish configuration. +
+
+
diff --git a/resources/views/woo/settings.blade.php b/resources/views/woo/settings.blade.php index d494428..866acad 100644 --- a/resources/views/woo/settings.blade.php +++ b/resources/views/woo/settings.blade.php @@ -2,6 +2,15 @@ Settings + +

See which WooCommerce marketing and shipping extensions are installed on your store.

+ + View integrations + + +
+

Connect WooCommerce stores from the WordPress plugin, then manage them here.

group(function () { Route::get('/stores', [StoreController::class, 'index'])->name('woo.stores.index'); Route::post('/stores/switch', StoreSwitchController::class)->name('woo.stores.switch'); Route::delete('/stores/{store}', [StoreController::class, 'destroy'])->name('woo.stores.destroy'); + Route::get('/integrations', [IntegrationController::class, 'index'])->name('woo.integrations.index'); Route::get('/settings', [SettingsController::class, 'edit'])->name('woo.settings'); Route::post('/ai/chat', [AiController::class, 'chat'])->middleware('throttle:30,1')->name('woo.ai.chat'); diff --git a/tests/Feature/WooIntegrationsTest.php b/tests/Feature/WooIntegrationsTest.php new file mode 100644 index 0000000..ae19d27 --- /dev/null +++ b/tests/Feature/WooIntegrationsTest.php @@ -0,0 +1,101 @@ +get(route('woo.integrations.index')) + ->assertRedirect(route('sso.connect', ['redirect' => route('woo.integrations.index')])); + } + + public function test_integrations_page_without_store_shows_catalog(): void + { + $user = User::factory()->create(); + + $this->actingAs($user) + ->get(route('woo.integrations.index')) + ->assertOk() + ->assertSee('Integrations') + ->assertSee('Google for WooCommerce') + ->assertSee('Pinterest for WooCommerce') + ->assertSee('Snapchat for WooCommerce') + ->assertSee('Connect a WooCommerce store'); + } + + public function test_integrations_page_fetches_status_from_plugin(): void + { + $user = User::factory()->create(); + $store = WooStore::create([ + 'user_id' => $user->id, + 'site_url' => 'https://shop.example.com', + 'site_name' => 'Example Shop', + 'status' => WooStore::STATUS_ACTIVE, + 'connected_at' => now(), + 'public_id' => 'store-public-id', + ]); + + Http::fake([ + 'https://shop.example.com/wp-json/ladill-woo/v1/integrations/status' => Http::response([ + 'integrations' => [ + 'shipping' => [ + 'state' => 'connected', + 'installed' => true, + 'connected' => true, + 'manage_url' => 'https://shop.example.com/wp-admin/admin.php?page=wc-settings&tab=shipping', + 'summary' => '2 zones, 4 methods', + 'details' => ['zones' => 2, 'methods' => 4], + ], + 'google' => [ + 'state' => 'needs_setup', + 'installed' => true, + 'connected' => false, + 'manage_url' => 'https://shop.example.com/wp-admin/admin.php?page=wc-admin&path=/google/start', + 'summary' => 'Installed — finish setup in WordPress.', + 'details' => [], + ], + 'pinterest' => [ + 'state' => 'not_installed', + 'installed' => false, + 'connected' => false, + 'manage_url' => null, + 'summary' => 'Extension is not installed on this store.', + 'details' => [], + ], + 'snapchat' => [ + 'state' => 'connected', + 'installed' => true, + 'connected' => true, + 'manage_url' => 'https://shop.example.com/wp-admin/admin.php?page=wc-admin&path=/snapchat/setup', + 'summary' => 'Connected and ready.', + 'details' => [], + ], + ], + ], 200), + ]); + + $this->actingAs($user) + ->get(route('woo.integrations.index')) + ->assertOk() + ->assertSee('Example Shop') + ->assertSee('2 zones, 4 methods') + ->assertSee('Needs setup') + ->assertSee('Not installed') + ->assertSee('Connected') + ->assertSee('Manage in WordPress'); + + Http::assertSent(function ($request) use ($store) { + return $request->url() === 'https://shop.example.com/wp-json/ladill-woo/v1/integrations/status' + && $request->hasHeader('X-Ladill-Store', $store->public_id); + }); + } +} diff --git a/tests/Feature/WooMobileNavTest.php b/tests/Feature/WooMobileNavTest.php index 9f35015..3b852c4 100644 --- a/tests/Feature/WooMobileNavTest.php +++ b/tests/Feature/WooMobileNavTest.php @@ -32,7 +32,7 @@ class WooMobileNavTest extends TestCase ->assertSee('>Profile', false); } - public function test_desktop_store_switcher_renders_after_search_form(): void + public function test_desktop_store_switcher_renders_before_afia(): void { $user = User::factory()->create(); WooStore::create([ @@ -48,11 +48,11 @@ class WooMobileNavTest extends TestCase ->assertOk() ->getContent(); - $searchPos = strpos($html, 'Search orders…'); $switcherPos = strpos($html, 'Example Shop'); + $afiaPos = strpos($html, 'afia'); - $this->assertNotFalse($searchPos); $this->assertNotFalse($switcherPos); - $this->assertLessThan($switcherPos, $searchPos); + $this->assertNotFalse($afiaPos); + $this->assertLessThan($afiaPos, $switcherPos); } }