/** * WordPress integration utilities. * * Handles the CP dashboard -> WP plugin REST bridge when running in embed mode: * 1. CDN URL / integration key sync to wp_options * 2. Bootstrap: read sync secret + wooActive status from the plugin * 3. Connect: POST credentials to customer-api /wordpress/connect, then * write twwim_tenant_id back to wp_options so the plugin's WC event * webhook pusher has a tenant to address * * (c) 2026 TWWIM UG. All rights reserved. (www.twwim.com) */ import { wordpressApi } from '@/infrastructure/http/api/wordpress'; const isWpEmbed = import.meta.env.VITE_WP_EMBED === 'true'; const wpCdnDefault = import.meta.env.VITE_WP_CDN_DEFAULT || 'https://cdn.twwim.ai'; interface ArcherAiConfig { restUrl: string; nonce: string; domain: string; siteUrl?: string; wooActive?: boolean; wooVersion?: string | null; } interface WpBootstrapResponse { siteUrl: string; syncSecret: string; wooActive: boolean; wooVersion: string | null; } function getWpConfig(): ArcherAiConfig | null { if (!isWpEmbed) return null; return (window as any).archerAi ?? null; } export function getWpDomain(): string { return getWpConfig()?.domain ?? ''; } export function isWordpressEmbed(): boolean { return isWpEmbed && getWpConfig() !== null; } export function isWooCommerceActive(): boolean { return !!getWpConfig()?.wooActive; } /** * Read every wp_option the plugin exposes via /twwim/v1/settings. * Returns null if not running in WP embed or on any fetch error. */ export async function wpGetSettings(): Promise | null> { const config = getWpConfig(); if (!config) return null; try { const res = await fetch(`${config.restUrl}settings`, { method: 'GET', headers: { 'X-WP-Nonce': config.nonce }, }); if (!res.ok) return null; return (await res.json()) as Record; } catch { return null; } } /** * Persist a new widget dock position to wp_options. Plugin sanitizer rejects * values outside the whitelist — caller should pass one of: * 'top-right' | 'middle-right' | 'bottom-right' | 'top-left' | 'bottom-left'. */ export async function wpSaveDockPosition(position: string): Promise { const config = getWpConfig(); if (!config) return false; try { const res = await fetch(`${config.restUrl}settings`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': config.nonce, }, body: JSON.stringify({ twwim_dock_position: position }), }); return res.ok; } catch { return false; } } /** * Sync CDN URL to wp_options on app boot. * Ensures the frontend widget injection uses the correct CDN for this build environment. */ export async function wpSyncCdnUrl(): Promise { const config = getWpConfig(); if (!config) return; try { await fetch(`${config.restUrl}settings`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': config.nonce, }, body: JSON.stringify({ twwim_cdn_url: wpCdnDefault }), }); } catch { // Non-fatal } } /** * Save integration key to WP options if the tenant domain matches the WP site domain. * Called after tenant creation or update. */ export async function wpAutoSaveKey( tenantDomain: string, integrationKeyPrefix: string, ): Promise { const config = getWpConfig(); if (!config) return; if (tenantDomain !== config.domain) return; try { await fetch(`${config.restUrl}settings`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': config.nonce, }, body: JSON.stringify({ twwim_integration_key: integrationKeyPrefix, twwim_cdn_url: wpCdnDefault, }), }); } catch { // Non-fatal — WP settings save failure shouldn't block tenant creation } } /** * Fetch integration keys for a tenant and auto-save production key to WP. * Uses the integration keys API to get the production key prefix. */ export async function wpAutoSaveKeyForTenant( tenantId: string, tenantDomain: string, listKeysFn: ( tenantId: string, ) => Promise<{ keys: Array<{ environment: string; keyPrefix: string }> }>, ): Promise { const config = getWpConfig(); if (!config) return; if (tenantDomain !== config.domain) return; try { const { keys } = await listKeysFn(tenantId); const productionKey = keys.find((k) => k.environment === 'PRODUCTION'); if (productionKey) { await wpAutoSaveKey(tenantDomain, productionKey.keyPrefix); } } catch { // Non-fatal } } /** * Read the sync secret + site URL + WC status from the WP plugin REST bootstrap route. * Requires WP admin nonce; only works when running inside WP admin. */ export async function wpBootstrap(): Promise { const config = getWpConfig(); if (!config) return null; try { const res = await fetch(`${config.restUrl}bootstrap`, { method: 'GET', headers: { 'X-WP-Nonce': config.nonce }, }); if (!res.ok) return null; const data = (await res.json()) as WpBootstrapResponse; if (!data.syncSecret || !data.siteUrl) return null; return data; } catch { return null; } } /** * Write back the customer-api URL + tenant id into wp_options so the plugin's * product webhook pusher has somewhere to POST. Called after successful connect. */ async function wpSaveSyncBinding(params: { apiUrl: string; tenantId: string; }): Promise { const config = getWpConfig(); if (!config) return; try { await fetch(`${config.restUrl}settings`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': config.nonce, }, body: JSON.stringify({ twwim_api_url: params.apiUrl, twwim_tenant_id: params.tenantId, }), }); } catch { // Non-fatal } } /** * Full auto-connect handshake for a tenant running in WP embed mode: * 1. GET /wp-json/twwim/v1/bootstrap → {siteUrl, syncSecret} * 2. POST customer-api /wordpress/connect → encrypted storage * 3. POST /wp-json/twwim/v1/settings with {twwim_api_url, twwim_tenant_id} * * Idempotent — safe to call on every mount. Returns true on success, false otherwise. */ export async function wpConnectSync(tenantId: string, tenantDomain: string): Promise { const config = getWpConfig(); if (!config) return false; if (tenantDomain !== config.domain) return false; const bootstrap = await wpBootstrap(); if (!bootstrap) return false; try { await wordpressApi.connect({ tenantId, siteUrl: bootstrap.siteUrl, syncSecret: bootstrap.syncSecret, }); } catch { return false; } const apiUrl = resolveCustomerApiUrl(); if (apiUrl) { await wpSaveSyncBinding({ apiUrl, tenantId }); } return true; } /** * Derive the customer-api base URL for the embedded dashboard. Prefers the * Vite build env (VITE_API_URL or VITE_CUSTOMER_API_URL), falls back to * the document origin. */ function resolveCustomerApiUrl(): string { const env = import.meta.env; return ( (env.VITE_API_URL as string | undefined) || (env.VITE_CUSTOMER_API_URL as string | undefined) || '' ).replace(/\/+$/, ''); }