array( label, description ). */ public static function categories() { $categories = array( 'getting-started' => array( 'label' => __( 'Getting started', 'convohub' ), 'description' => __( 'What the plugin does and the first steps.', 'convohub' ), ), 'connection' => array( 'label' => __( 'Connection', 'convohub' ), 'description' => __( 'Connection key, account and disconnecting.', 'convohub' ), ), 'widget' => array( 'label' => __( 'Chat widget', 'convohub' ), 'description' => __( 'Inbox, allowed domains, pages and looks.', 'convohub' ), ), 'identity' => array( 'label' => __( 'Identity validation', 'convohub' ), 'description' => __( 'Recognizing logged-in customers.', 'convohub' ), ), 'leads' => array( 'label' => __( 'Lead forms', 'convohub' ), 'description' => __( 'Fields, styling and retries.', 'convohub' ), ), 'webhooks' => array( 'label' => __( 'Webhooks and replies', 'convohub' ), 'description' => __( 'How ConvoHub tells your site about messages.', 'convohub' ), ), 'privacy' => array( 'label' => __( 'Privacy and data', 'convohub' ), 'description' => __( 'What is sent, what is stored.', 'convohub' ), ), 'troubleshooting' => array( 'label' => __( 'Troubleshooting', 'convohub' ), 'description' => __( 'Common problems and fixes.', 'convohub' ), ), ); /** * Filters the help topics. * * @param array[] $categories Id => array( 'label' => string, 'description' => string ). */ $filtered = apply_filters( 'convohub_help_categories', $categories ); if ( ! is_array( $filtered ) ) { return $categories; } $clean = array(); foreach ( $filtered as $id => $category ) { $id = sanitize_key( $id ); if ( '' === $id || ! is_array( $category ) || empty( $category['label'] ) ) { continue; } $clean[ $id ] = array( 'label' => sanitize_text_field( $category['label'] ), 'description' => sanitize_text_field( $category['description'] ?? '' ), ); } return $clean ? $clean : $categories; } /** * The help articles, after add-ons had their say and everything was cleaned. * * @return array[] Each: id, category, title, keywords (text), body (limited HTML). */ public static function articles() { $defaults = self::default_articles(); /** * Filters the help articles. Add yours to the list. * * Each article is an array with: `id` (unique slug), `category` (a topic id), `title` (plain text), * `keywords` (text, or a list of words, that search matches in addition to the title and body) and * `body` (HTML limited to p, ul, ol, li, strong, em, code, h4, br and links; anything else is removed). * Entries that are invalid, or that reuse an existing id, are dropped. * * @param array[] $articles Articles. */ $filtered = apply_filters( 'convohub_help_articles', $defaults ); if ( ! is_array( $filtered ) ) { $filtered = $defaults; } $categories = self::categories(); $allowed = self::allowed_tags(); $seen = array(); $articles = array(); foreach ( $filtered as $article ) { if ( ! is_array( $article ) ) { continue; } $id = sanitize_key( $article['id'] ?? '' ); $category = sanitize_key( $article['category'] ?? '' ); $title = sanitize_text_field( is_scalar( $article['title'] ?? null ) ? (string) $article['title'] : '' ); $body = trim( wp_kses( is_scalar( $article['body'] ?? null ) ? (string) $article['body'] : '', $allowed ) ); if ( '' === $id || isset( $seen[ $id ] ) || ! isset( $categories[ $category ] ) || '' === $title || '' === trim( wp_strip_all_tags( $body ) ) ) { continue; } $keywords = $article['keywords'] ?? ''; $keywords = is_array( $keywords ) ? implode( ' ', array_filter( $keywords, 'is_scalar' ) ) : ( is_scalar( $keywords ) ? (string) $keywords : '' ); $seen[ $id ] = true; $articles[] = array( 'id' => $id, 'category' => $category, 'title' => $title, 'keywords' => mb_substr( sanitize_text_field( $keywords ), 0, 300 ), 'body' => $body, ); }//end foreach return $articles; } /** * Markup that article bodies may use. * * @return array */ private static function allowed_tags() { return array( 'p' => array(), 'ul' => array(), 'ol' => array(), 'li' => array(), 'strong' => array(), 'em' => array(), 'code' => array(), 'h4' => array(), 'br' => array(), 'a' => array( 'href' => true, 'target' => true, 'rel' => true, ), ); } /** * Address of the documentation, or an empty string when none is configured. Set it with the * CONVOHUB_DOCS_URL constant or the `convohub_docs_url` filter. * * @return string */ public static function docs_url() { return self::clean_url( apply_filters( 'convohub_docs_url', defined( 'CONVOHUB_DOCS_URL' ) ? CONVOHUB_DOCS_URL : '' ) ); } /** * Address of the support page, or an empty string when none is configured. Set it with the * CONVOHUB_SUPPORT_URL constant or the `convohub_support_url` filter. * * @return string */ public static function support_url() { return self::clean_url( apply_filters( 'convohub_support_url', defined( 'CONVOHUB_SUPPORT_URL' ) ? CONVOHUB_SUPPORT_URL : '' ) ); } /** * Keeps a configured web address only when it is a plain http(s) URL. * * @param mixed $url Value from a filter. * @return string */ private static function clean_url( $url ) { return is_string( $url ) ? esc_url_raw( trim( $url ), array( 'http', 'https' ) ) : ''; } /** * Adds a short help tab to the WordPress Help drawer of the ConvoHub screen. */ public static function add_screen_help() { $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; if ( ! $screen ) { return; } $content = '
' . esc_html__( 'ConvoHub connects this site to your ConvoHub account: the chat widget, recognized customers, lead forms and webhooks.', 'convohub' ) . '
'; $content .= '' . sprintf( /* translators: %s: link to the Help tab, with its text. */ esc_html__( 'Search the guides, follow the setup guide and copy diagnostics for support on the %s.', 'convohub' ), '' . esc_html__( 'ConvoHub Help tab', 'convohub' ) . '' ) . '
'; $screen->add_help_tab( array( 'id' => 'convohub-help', 'title' => __( 'ConvoHub help', 'convohub' ), 'content' => $content, ) ); } /** * Prints the Help tab: a search field, the setup list, the articles by topic, then diagnostics. * * @param array $settings Plugin settings. */ public static function render( array $settings ) { $categories = self::categories(); $articles = self::articles(); $grouped = array(); foreach ( $articles as $article ) { $grouped[ $article['category'] ][] = $article; } $steps = ConvoHub_Setup::steps( $settings ); $rows = ConvoHub_Diagnostics::rows( $settings, ConvoHub_Diagnostics::environment() ); $docs = self::docs_url(); $help = self::support_url(); ?>
' . __( 'Work through the tabs from left to right. The setup list on the Connection tab checks each step from your saved settings.', 'convohub' ) . '
' . '' . __( 'To collect leads with a form, also choose an API inbox on the Leads tab and add the form to a page.', 'convohub' ) . '
' ), $article( 'what-it-does', 'getting-started', __( 'What this plugin does', 'convohub' ), 'overview features about widget identity leads webhook commerce woocommerce', '' . __( 'ConvoHub for WordPress connects your site to your ConvoHub account.', 'convohub' ) . '
' . '' . __( 'The ConvoHub Commerce add-on builds on this plugin for WooCommerce cart recovery and order-aware support. It needs this plugin to be connected first.', 'convohub' ) . '
' ), $article( 'tabs-tour', 'getting-started', __( 'Where to find things', 'convohub' ), 'tabs menu navigation connection widget leads logs help where', '' . __( 'Use the arrow keys to move between the tabs when one has focus.', 'convohub' ) . '
' ), $article( 'requirements', 'getting-started', __( 'What you need before you start', 'convohub' ), 'requirements account api access administrator https php wordpress version prerequisites signup trial create', '' . __( 'In ConvoHub open Settings > Integrations > WordPress and generate a key. Copy the whole key and paste it into "Connection key" on the Connection tab.', 'convohub' ) . '
' . '' . sprintf( __( 'No ConvoHub account yet? Create one first. New accounts include a 7-day trial.', 'convohub' ), esc_url( ConvoHub_Settings::signup_url() ) ) . '
' . '' . __( 'The account is the one the key was made for. There is nothing to choose and nothing else to save. "Test connection" checks later that the connection still works.', 'convohub' ) . '
' . '' . __( 'To use another account, disconnect this site and connect again with a key from that account. Connecting to a different account clears the inbox, identity validation and webhook settings, because they belong to the old account. Connecting again to the same account keeps them.', 'convohub' ) . '
' ), $article( 'key-address', 'connection', __( 'This plugin connects to app.convohub.io', 'convohub' ), 'key address url https app.convohub.io invalid', '' . __( 'This plugin always connects to https://app.convohub.io. There is no address to type, and a saved address is not used.', 'convohub' ) . '
' . __( 'If you see "This plugin only connects to app.convohub.io. Generate a new key there and paste it again.", the key was made for a different address. Generate a new key at https://app.convohub.io.', 'convohub' ) . '
' ), $article( 'replace-token', 'connection', __( 'Replace an access token with a connection key', 'convohub' ), 'token legacy replace access token upgrade older version reconnect migrate', '' . __( 'Versions before 0.4.0 asked for an access token. A site connected that way keeps working, and the Connection tab shows a notice until you replace the token. The plugin always connects to app.convohub.io: there is no address to enter.', 'convohub' ) . '
' . '' . __( 'If the key belongs to the same account, the inbox, identity validation and webhook settings stay as they are.', 'convohub' ) . '
' ), $article( 'disconnect-site', 'connection', __( 'What Disconnect does', 'convohub' ), 'disconnect remove connection forget revoke leave account', '' . __( 'Disconnect, at the bottom of the Connection tab, makes this site forget the connection: the access it was given, the account, the chosen inboxes and the webhook settings. The chat widget, lead forms and webhook stop working until you connect again.', 'convohub' ) . '
' . '' . __( 'Nothing is deleted in ConvoHub and ConvoHub is not contacted. The webhook stays registered there, so press "Remove" on the webhook first if you want it gone, and remove the access in ConvoHub if you want it revoked.', 'convohub' ) . '
' ), $article( 'api-access', 'connection', __( 'API access is not enabled for this account', 'convohub' ), '403 api access enabled account error forbidden permission', '' . __( 'This message means ConvoHub accepted the connection but the account does not have API access turned on. The plugin cannot work around that.', 'convohub' ) . '
' . '' . __( 'Ask the person who runs your ConvoHub account to enable API access for it, then test the connection again.', 'convohub' ) . '
' ), // Chat widget. $article( 'choose-inbox', 'widget', __( 'Choose or create the website inbox', 'convohub' ), 'inbox website create choose widget select', '' . __( 'The chat widget belongs to a website inbox in your ConvoHub account. On the Widget tab, pick an existing one and press "Use this inbox", or press "Create an inbox for this site" to make one named after your site.', 'convohub' ) . '
' . '' . __( 'The widget cannot be switched on until an inbox is chosen. Colors and the welcome text are edited on the inbox in ConvoHub, not here.', 'convohub' ) . '
' ), $article( 'allowed-domains', 'widget', __( 'Allowed domains: why the chat may not show', 'convohub' ), 'allowed domains blocked cors origin browser inbox domain add this site not showing', '' . __( 'A website inbox can be limited to certain websites. If your site is not on its list, the browser refuses to load the chat and nothing appears.', 'convohub' ) . '
' . '' . __( 'The Widget tab checks this for you. When your site is not allowed it shows a warning with the button "Add this site to the inbox", which adds your site\'s address to the inbox in ConvoHub.', 'convohub' ) . '
' . 'https://shop.example.com, a host such as shop.example.com, a wildcard such as *.example.com, or * for everything.', 'convohub' ) . '*.example.com matches subdomains such as shop.example.com, but not example.com itself.', 'convohub' ) . '' . __( 'Under Display on the Widget tab, "Show on" chooses between all pages, only the pages you list, or all pages except the ones you list.', 'convohub' ) . '
' . '/pricing or /shop/*. The * matches anything, and the home page is /.', 'convohub' ) . 'convohub_widget_should_display filter.', 'convohub' ) . '' . __( 'Each option can be left on "ConvoHub default", which uses whatever the inbox is set to in ConvoHub, or set here for this site only.', 'convohub' ) . '
' . '' . __( 'You can choose the side the chat sits on, the launcher style, a label for the expanded launcher, the color scheme, and whether visitors can upload files, use the emoji picker, end the conversation, or get the chat in their browser\'s language. Brand colors and the welcome message are set on the inbox in ConvoHub.', 'convohub' ) . '
' ), // Identity validation. $article( 'identity-validation', 'identity', __( 'How identity validation works', 'convohub' ), 'identity validation verify hmac secret logged in customers recognize signature security', '' . __( 'When a logged-in customer opens a page with the chat, your site signs the customer\'s ID on the server with the inbox identity secret and gives the chat the ID, name, email address and signature. ConvoHub checks the signature, so it can link the chat to that customer and show their conversation history on any device.', 'convohub' ) . '
' . '' . __( 'Switch it on with "Recognize logged-in customers in the chat" on the Widget tab.', 'convohub' ) . '
' ), $article( 'identity-mandatory', 'identity', __( 'Why mandatory identity validation matters', 'convohub' ), 'mandatory enforce identity validation impersonate spoof security hmac inbox settings required', '' . __( 'Signing customers on your site is only half of the protection. The other half is telling ConvoHub to insist on it.', 'convohub' ) . '
' . '' . __( 'If identity validation is not required for the inbox, a curious visitor can open their browser tools, claim another customer\'s ID or email address, and be shown that person\'s conversation history. Turning validation on for the inbox in ConvoHub makes it reject any identity not signed with your secret.', 'convohub' ) . '
' . '' . __( 'The plugin sets a small cookie named convohub_logged_in when someone logs in and removes it when they log out. The chat uses it to know when to ask your site who is signed in, so full-page caching cannot show one person\'s identity to another.', 'convohub' ) . '
' . __( 'The reply to that question is never cached. If a cache plugin or CDN caches it anyway, exclude the WordPress admin-ajax.php action convohub_identity from caching.', 'convohub' ) . '
' . __( 'In the block editor, add the "ConvoHub Lead Form" block. In the classic editor or any text area that supports shortcodes, paste:', 'convohub' ) . '
' . '[convohub_form heading="Contact us" button="Send"]
' . __( 'Both attributes are optional: heading is the title above the form and button is the text on the send button. Which fields appear is chosen on the Leads tab under Form fields.', 'convohub' ) . '
' . __( 'Each submission creates a contact and a conversation in an API inbox of your ConvoHub account. On the Leads tab choose an existing API inbox, or press "Create an inbox for leads".', 'convohub' ) . '
' . '' . __( 'Until an inbox is chosen, submitted leads cannot be delivered and wait in the Logs tab.', 'convohub' ) . '
' ), $article( 'lead-fields', 'leads', __( 'Choose and add form fields', 'convohub' ), 'fields editor custom field company budget required dropdown checkbox type name label order', '' . __( 'Under Form fields on the Leads tab you can show or hide each field, rename it, mark it required and change the order. Name, Email, Phone and Message always exist; you can hide them but not delete them.', 'convohub' ) . '
' . '' . __( 'ConvoHub needs phone numbers in international format, which starts with + and the country code. The form converts what visitors type.', 'convohub' ) . '
' . '' . __( 'Numbers typed without a + use the default country code from the Leads tab. Enter only the digits of the calling code, for example 91 for India or 1 for the United States. If it is empty, visitors must type the + and their country code themselves.', 'convohub' ) . '
' . __( 'The Appearance section on the Leads tab sets the button colour, the corners, the space between fields and the maximum width. What you leave on "Follow the theme" keeps the look of your theme. It is only loaded on pages that show the form.', 'convohub' ) . '
' . '' . __( 'For anything else, use WordPress\'s own Additional CSS: Appearance > Customize > Additional CSS, or Styles in the Site Editor. The form has these class names:', 'convohub' ) . '
' . '.convohub-lead-form, .convohub-lead-form__heading.convohub-lead-form__field, .convohub-lead-form__field--email ' . __( '(or any field name)', 'convohub' ) . '.convohub-lead-form__required, .convohub-lead-form__error, .convohub-lead-form__success.convohub-lead-form button' . __( 'If ConvoHub cannot be reached when a visitor sends the form, the lead is not lost. It is kept on your site and sent again automatically.', 'convohub' ) . '
' . '' . __( 'The Logs tab only lists leads that are not delivered. Delivered leads are removed from your site, and anything left is deleted after 30 days.', 'convohub' ) . '
' ), $article( 'spam-protection', 'leads', __( 'Spam protection and the submission limit', 'convohub' ), 'spam honeypot rate limit too many submissions proxy cdn ip captcha bot', '' . __( 'The form has a hidden field that people never see but simple bots fill in; those submissions are ignored. Each visitor address may send 5 submissions every 10 minutes, and each submission may be at most 32 KB.', 'convohub' ) . '
' . '' . __( 'The limit uses the visitor\'s IP address. Behind a proxy or CDN every visitor can look the same, so real visitors may see "too many submissions". Developers can return the real address with the convohub_client_ip filter.', 'convohub' ) . '
' . __( 'A webhook lets ConvoHub call your site when something happens. This plugin registers one that ConvoHub uses to report new customer messages (message_incoming) and changes in a conversation\'s status (conversation_status_changed).', 'convohub' ) . '
' . __( 'On its own the core plugin only checks each event and passes it on to other code on your site through the convohub_webhook_event action. Add-ons such as ConvoHub Commerce use it. Without an add-on, nothing visible happens on your site; your conversations still live in ConvoHub.', 'convohub' ) . '
' . __( 'Anyone can send a request to a public address, so every event carries a signature that only ConvoHub and your site can produce. The plugin accepts an event only when:', 'convohub' ) . '
' . 'X-Chatwoot-Signature, an HMAC-SHA256 of the timestamp and the body, made with the webhook secret) matches.', 'convohub' ) . '' . __( 'Anything else is answered with a plain "Rejected" (HTTP 401) and no reason, so a caller cannot probe the check. The secret is created when you register the webhook and stays on your server.', 'convohub' ) . '
' ), $article( 'webhook-register', 'webhooks', __( 'Register, update or remove the webhook', 'convohub' ), 'register webhook update remove check connection tab receiver address public https', '' . __( 'On the Connection tab, under Webhook, "Register webhook" creates the webhook in ConvoHub for this site and saves its secret.', 'convohub' ) . '
' . '' . __( 'The core plugin does not read or act on message text. A customer\'s reply, including a word such as STOP, is delivered to ConvoHub, shown in your inbox, and reported to your site as a message_incoming event.', 'convohub' ) . '
' . __( 'What that event means is up to the code that listens to it. Add-ons, such as ConvoHub Commerce, can decide that a STOP reply means "send this person no more messages". Check that add-on\'s own help for what it does. If nothing listens, the event is simply received and ignored.', 'convohub' ) . '
' . '' . __( 'Opt-out rules for WhatsApp and SMS are also enforced by ConvoHub and the messaging provider, not by this plugin.', 'convohub' ) . '
' ), // Privacy and data. $article( 'data-sent', 'privacy', __( 'What data is sent to ConvoHub', 'convohub' ), 'data sent privacy gdpr personal information external service token email phone', 'convohub_settings. They are never printed on any page. The connection key itself is not stored; only its first 12 characters are kept, to show which key was used.', 'convohub' ) . 'convohub_logged_in cookie, and local and session storage entries that let the chat notice a change of user and remember campaign details.', 'convohub' ) . '' . __( 'The plugin adds suggested text to Settings, Privacy, Policy Guide, which you can edit and use in your privacy policy.', 'convohub' ) . '
' . '' . __( 'WordPress\'s Export Personal Data and Erase Personal Data tools include leads that are still waiting to be delivered, matched by email address. Leads that already reached ConvoHub live in your ConvoHub account, so export or erase those there.', 'convohub' ) . '
' ), $article( 'uninstall', 'privacy', __( 'What happens when you uninstall', 'convohub' ), 'uninstall delete remove deactivate data cleanup', '' . __( 'Deactivating the plugin keeps everything. Deleting it from the Plugins screen removes its settings (including the token and secrets), undelivered leads, scheduled jobs and temporary data from your database.', 'convohub' ) . '
' . '' . __( 'Check these in order:', 'convohub' ) . '
' . '' . __( '"This key is not valid. It may have been used already, revoked or expired." ConvoHub gives the same answer in every one of these cases, so it cannot say which one it was.', 'convohub' ) . '
' . '' . __( 'Other messages: "Too many attempts" is this site limiting tries (10 every 10 minutes); "ConvoHub received too many connection attempts" comes from ConvoHub; "read-only because of its billing status" means the ConvoHub account needs its billing fixed; "did not accept this site\'s address" means the WordPress Address and Site Address in Settings > General are not a public address; "newer way of signing in" means this plugin should be updated.', 'convohub' ) . '
' ), $article( 'connection-errors', 'troubleshooting', __( 'Connection test errors and what they mean', 'convohub' ), 'error test connection rejected access 401 402 404 429 500 unreachable timeout billing rate limit message', '' . __( 'Check these in order:', 'convohub' ) . '
' . '/wp-json/convohub/v1/webhook, and the REST API is not disabled for visitors.', 'convohub' ) . '' . __( 'Open the Logs tab. A lead that could not be delivered is listed with its last error.', 'convohub' ) . '
' . '' . __( 'Automatic retries run on WP-Cron, which WordPress triggers when someone visits your site. On a very quiet site retries can be late.', 'convohub' ) . '
' . '' . __( 'If your site sets DISABLE_WP_CRON to true, WordPress no longer triggers it by itself and a real cron job must run wp-cron.php, otherwise retries never happen. The Diagnostics panel shows which case you are in. Resend on the Logs tab works either way.', 'convohub' ) . '
convohub_identity request.', 'convohub' ) . '' . __( 'The form allows 5 submissions per visitor address every 10 minutes. If your site sits behind a proxy or CDN, every visitor can share one address and hit the limit together.', 'convohub' ) . '
' . '' . __( 'Ask your developer to return the real visitor address with the convohub_client_ip filter, using the header your proxy sets.', 'convohub' ) . '