state, title, message, action, notes, stale. */ public static function compute( Connection $connection, Stand $stand ): array { $settings = Settings::all(); $domain = Environment::home_domain(); $check = Installation_Check::last(); $notes = array(); if ( 'consent' === $settings['consent']['mode'] ) { $notes[] = __( 'Chat loads after cookie consent', 'stand-chat' ); } if ( $connection->needs_reconnect() ) { return self::state( 'reconnect', __( 'Reconnect needed', 'stand-chat' ), Api_Client::reconnect_message(), self::action( __( 'Reconnect', 'stand-chat' ), 'reconnect' ), $notes ); } if ( ! $connection->is_connected() ) { if ( '' === $settings['site_id'] ) { return self::state( 'setup', __( 'Not connected', 'stand-chat' ), '', null, $notes ); } return self::local_status( $settings, $check, $notes ); } $site = $stand->site(); if ( is_array( $site ) && isset( $site['installed'] ) && ! $site['installed'] ) { // Stand marks the Site installed within a minute of the first visit: // while waiting, read it again rather than from the cache. $site = $stand->site( true ); } if ( is_wp_error( $site ) && 'stand_reconnect_needed' === $site->get_error_code() ) { // Stand just rejected the connection. return self::state( 'reconnect', __( 'Reconnect needed', 'stand-chat' ), Api_Client::reconnect_message(), self::action( __( 'Reconnect', 'stand-chat' ), 'reconnect' ), $notes ); } if ( is_wp_error( $site ) && ! Api_Client::is_temporary( $site ) ) { // Stand answered, but not with this Site: it was removed in Stand, // or this account can't use it. Chat being live can't be told. return self::state( 'unknown', __( 'Stand can\'t show this site', 'stand-chat' ), $site->get_error_message(), self::action( __( 'Reconnect', 'stand-chat' ), 'reconnect' ), $notes ); } $stale = is_wp_error( $site ); // While Stand can't be reached, or fails or is busy for now, the Site as // WordPress last saw it, and nothing more is asked of Stand, however that // was found out: each try could wait for its timeout. $offline = $stale ? $site : null; $ask = static function ( callable $call ) use ( &$offline, &$stale ) { if ( null !== $offline ) { return $offline; } $answer = $call(); if ( is_wp_error( $answer ) && Api_Client::is_temporary( $answer ) ) { $offline = $answer; $stale = true; } return $answer; }; $site = is_wp_error( $site ) ? (array) ( Connection::stored_metadata()['site'] ?? array() ) : $site; if ( ! $settings['chat_enabled'] || ( isset( $site['enabled'] ) && ! $site['enabled'] ) ) { $off_in_stand = isset( $site['enabled'] ) && ! $site['enabled']; return self::state( 'off', __( 'Stand is off on this site', 'stand-chat' ), $off_in_stand ? __( 'The Site is turned off in Stand, so chat is off on every website that uses this Site ID.', 'stand-chat' ) : __( 'Your settings are kept. Visitors see no chat until you turn it back on.', 'stand-chat' ), State::setup()['live'] || $off_in_stand ? self::action( __( 'Turn on', 'stand-chat' ), $off_in_stand ? 'enable_site' : 'enable_chat' ) : self::action( __( 'Finish setup', 'stand-chat' ), 'setup' ), $notes, $stale ); } $hidden = Environment::hidden_reason(); if ( '' !== $hidden ) { return self::hidden_status( $hidden, $notes, $stale ); } if ( Environment::prints_elsewhere( $settings, Environment::is_production() ) ) { return self::state( 'hidden_environment', __( 'Chat doesn\'t show on this address', 'stand-chat' ), sprintf( /* translators: 1: WordPress address, 2: Stand Site domain */ __( 'This copy prints the chat script, as set, but it runs on %1$s and Stand only shows chat on %2$s.', 'stand-chat' ), Environment::home_domain(), Environment::site_domain() ), self::action( __( 'Environment settings', 'stand-chat' ), 'settings_advanced' ), $notes, $stale ); } $plan = $ask( array( $stand, 'plan' ) ); if ( ! is_wp_error( $plan ) ) { $usage = $plan['usage']; $quota = $usage['chatQuota'] ?? null; if ( null !== $quota && (int) $usage['chatCount'] >= (int) $quota && (int) ( $usage['chatsBalance'] ?? 0 ) <= 0 ) { $on_base = 'base' === ( $plan['plan']['planCode'] ?? '' ); return self::state( 'quota', __( 'New chats are paused', 'stand-chat' ), sprintf( /* translators: 1: chat quota, 2: reset date */ __( 'All %1$d of this period\'s chats and any extra chats are used, so visitors can\'t start a new chat until %2$s. Chats already underway continue.', 'stand-chat' ), (int) $quota, self::date( (string) $usage['periodEnd'] ) ), $connection->is_owner() && $on_base ? self::action( __( 'Start 7-day Pro trial', 'stand-chat' ), 'trial' ) : self::action( __( 'See plans', 'stand-chat' ), 'plans' ), $notes, $stale ); } } $who = $ask( array( $stand, 'who_answers' ) ); $matches = $ask( array( $stand, 'matches' ) ); if ( ! is_wp_error( $who ) && ! is_wp_error( $matches ) && empty( $matches ) && in_array( $who['mode'], array( 'only_you', 'nobody' ), true ) ) { return self::state( 'nobody', __( 'No one is answering yet', 'stand-chat' ), __( 'Who answers is set to Only you, and you\'re not Available in the Stand app, so visitors see no chat.', 'stand-chat' ), self::action( __( 'Let your AI assistant answer', 'stand-chat' ), 'who_answers' ), $notes, $stale ); } if ( isset( $site['installed'] ) && ! $site['installed'] ) { $found = $check && 'found' === ( $check['status'] ?? '' ); return self::state( 'waiting', __( 'Waiting for installation', 'stand-chat' ), $found /* translators: %s: address of the page checked */ ? sprintf( __( 'Stand script found on %s. Open your site once; Stand shows it as installed within a minute.', 'stand-chat' ), self::checked_page( $check ) ) : self::check_message( $check ), // A page where chat loads, which may not be the home page. self::action( __( 'Open your site', 'stand-chat' ), 'view_site', Installation_Check::page_to_check() ), $notes, $stale ); } $names = ! is_wp_error( $who ) ? array_filter( (array) $who['answering'] ) : array(); $name = empty( $names ) ? __( 'Your AI assistant', 'stand-chat' ) : reset( $names ); $mode = is_wp_error( $who ) ? '' : $who['mode']; if ( is_wp_error( $who ) ) { // No mode to describe: nobody is known to answer, nor not to. $message = __( 'Stand couldn\'t say who answers new chats just now.', 'stand-chat' ); } elseif ( in_array( $mode, array( 'only_you', 'nobody' ), true ) ) { $message = __( 'New chats come to you while you\'re Available in the Stand app.', 'stand-chat' ); } elseif ( 'ai_every' === $mode ) { /* translators: %s: AI assistant name */ $message = sprintf( __( '%s answers every new chat.', 'stand-chat' ), $name ); } else { /* translators: %s: AI assistant name */ $message = sprintf( __( '%s answers whenever you\'re away.', 'stand-chat' ), $name ); } return self::state( 'live', /* translators: %s: domain */ sprintf( __( 'Chat is live on %s', 'stand-chat' ), $domain ), $message, self::action( __( 'View your site', 'stand-chat' ), 'view_site', home_url( '/' ) ), $notes, $stale ); } /** * Status with only a pasted Site ID (no connection). * * @param array $settings Settings. * @param array|null $check Last installation check. * @param string[] $notes Notes. * @return array */ private static function local_status( array $settings, ?array $check, array $notes ): array { if ( ! $settings['chat_enabled'] ) { return self::state( 'off', __( 'Stand is off on this site', 'stand-chat' ), __( 'Your settings are kept. Visitors see no chat until you turn it back on.', 'stand-chat' ), self::action( __( 'Turn on', 'stand-chat' ), 'enable_chat' ), $notes ); } $hidden = Environment::hidden_reason(); if ( '' !== $hidden ) { return self::hidden_status( $hidden, $notes, false ); } $found = $check && 'found' === ( $check['status'] ?? '' ); return self::state( $found ? 'printing' : 'waiting', $found ? __( 'Stand is on your site', 'stand-chat' ) : __( 'Waiting for installation', 'stand-chat' ), $found /* translators: %s: address of the page checked */ ? sprintf( __( 'Stand script with this Site ID found on %s. Open your site once, then check Installed on the site card in Stand.', 'stand-chat' ), self::checked_page( $check ) ) : self::check_message( $check ), self::action( __( 'View your site', 'stand-chat' ), 'view_site', Installation_Check::page_to_check() ), $notes ); } /** * "Chat is hidden on this environment". * * @param string $reason non_production or domain_mismatch. * @param string[] $notes Notes. * @param bool $stale Stand unreachable. * @return array */ private static function hidden_status( string $reason, array $notes, bool $stale ): array { $site_domain = Environment::site_domain(); $message = 'domain_mismatch' === $reason ? sprintf( /* translators: 1: WordPress address, 2: Stand Site domain */ __( 'This copy runs on %1$s. Stand only shows chat on %2$s.', 'stand-chat' ), Environment::home_domain(), $site_domain ) : sprintf( /* translators: %s: environment type */ __( 'This WordPress is set up as %s, so chat is hidden here. Production sites show chat.', 'stand-chat' ), Environment::type() ); return self::state( 'hidden_environment', __( 'Chat is hidden on this environment', 'stand-chat' ), $message, 'domain_mismatch' === $reason ? self::action( __( 'Review', 'stand-chat' ), 'address' ) : self::action( __( 'Environment settings', 'stand-chat' ), 'settings_advanced' ), $notes, $stale ); } /** * Copy for an installation check result. * * @param array|null $check Check. */ public static function check_message( ?array $check ): string { $page = self::checked_page( $check ); switch ( $check['status'] ?? '' ) { case 'found': /* translators: %s: address of the page checked */ return sprintf( __( 'Stand script with this Site ID found on %s.', 'stand-chat' ), $page ); case 'consent_loader': return __( 'Chat loads on your pages after cookie consent.', 'stand-chat' ); case 'missing': /* translators: %s: address of the page checked */ return sprintf( __( 'The Stand script isn\'t on %s. A page cache may still serve an older copy: clear it, then check again. Your theme also needs to call wp_footer().', 'stand-chat' ), $page ); case 'wrong_site_id': /* translators: %s: address of the page checked */ return sprintf( __( 'The Stand script on %s has another Site ID. Clear your page cache, then check again.', 'stand-chat' ), $page ); case 'duplicate': /* translators: %s: address of the page checked */ return sprintf( __( 'Another Stand script is on %s, for example in your theme\'s header. Remove it so the chat loads once.', 'stand-chat' ), $page ); case 'unreachable': /* translators: %s: address of the page checked */ return sprintf( __( 'WordPress couldn\'t load %s to check. Open your site once to finish.', 'stand-chat' ), $page ); default: return __( 'Open your site once to finish.', 'stand-chat' ); } } /** * The address of the page an installation check loaded, without the * scheme: the home page, unless Where chat appears leaves it out. * * @param array|null $check Check. */ public static function checked_page( ?array $check ): string { $url = (string) ( $check['url'] ?? '' ); return untrailingslashit( (string) preg_replace( '#^https?://#i', '', '' !== $url ? $url : home_url( '/' ) ) ); } /** * Builds a status. * * @param string $state State key. * @param string $title Title. * @param string $message Message. * @param array|null $action Primary action. * @param string[] $notes Informational lines. * @param bool $stale Stand unreachable; data may be old. * @return array */ private static function state( string $state, string $title, string $message, ?array $action, array $notes, bool $stale = false ): array { return array( 'state' => $state, 'title' => $title, 'message' => $message, 'action' => $action, 'notes' => $notes, 'stale' => $stale, ); } /** * An action button. * * @param string $label Label. * @param string $kind What it does; the admin app maps it to a route or call. * @param string $href Link, when it's a link. * @return array */ private static function action( string $label, string $kind, string $href = '' ): array { return array( 'label' => $label, 'kind' => $kind, 'href' => $href, ); } /** * A date in the site's format, such as "Mon 20 Oct". * * @param string $iso ISO 8601 date. */ public static function date( string $iso ): string { $time = strtotime( $iso ); return $time ? wp_date( 'D j M', $time ) : $iso; } }