in_array( $choice, array( 'allowed', 'declined' ), true ) ? $choice : '', 'id' => (bool) preg_match( '/^[a-f0-9]{32}$/D', $id ) ? $id : '', 'allowed_at' => (int) ( $raw['allowed_at'] ?? 0 ), 'last_sent' => (int) ( $raw['last_sent'] ?? 0 ), ); } /** * Whether usage info is being shared. */ public static function allowed(): bool { $state = self::state(); return 'allowed' === $state['choice'] && '' !== $state['id']; } /** * Whether the notice is shown now: administrators, no answer yet, and not while the account card asks something. */ public static function eligible(): bool { return OwnKeySection::can_manage() && '' === self::state()['choice'] && ! OptIn::eligible(); } /** * «Allow»: a new random id, one call now, the weekly call scheduled. * * @param int|null $now Unix time. */ public static function allow( ?int $now = null ): void { $now = $now ?? time(); update_option( self::OPTION, array( 'choice' => 'allowed', 'id' => bin2hex( random_bytes( 16 ) ), 'allowed_at' => $now, 'last_sent' => 0, ), false ); self::send( 'optin', $now ); if ( ! wp_next_scheduled( self::HOOK ) ) { wp_schedule_event( $now + WEEK_IN_SECONDS, 'weekly', self::HOOK ); } } /** * «No thanks» (notice) or «Turn off» (Settings): stops the weekly call and deletes the id. */ public static function decline(): void { wp_clear_scheduled_hook( self::HOOK ); update_option( self::OPTION, array( 'choice' => 'declined' ), false ); } /** * Deactivation: stops the weekly call and deletes the id. A «No thanks» is kept; an «Allow» is forgotten. */ public static function deactivate(): void { wp_clear_scheduled_hook( self::HOOK ); if ( 'declined' === self::state()['choice'] ) { return; } delete_option( self::OPTION ); } /** * The weekly WP-Cron call (does nothing unless an administrator allowed it). * * @param int|null $now Unix time. */ public static function heartbeat( ?int $now = null ): void { if ( ! self::allowed() ) { wp_clear_scheduled_hook( self::HOOK ); return; } $now = $now ?? time(); if ( $now - self::state()['last_sent'] < self::MIN_GAP ) { return; } self::send( 'heartbeat', $now ); } /** * What is sent — the whole body (tests check its keys). * * @param string $event optin | heartbeat. * @return array */ public static function payload( string $event ): array { return array( 'install_id' => self::state()['id'], 'event' => $event, 'plugin_version' => (string) ADDSYNAPSE_ASSISTANT_VERSION, 'wp_version' => (string) get_bloginfo( 'version' ), 'wc_version' => defined( 'WC_VERSION' ) ? (string) WC_VERSION : '', 'locale' => (string) get_locale(), 'consent_version' => self::CONSENT_VERSION, ); } /** * One call (failures are ignored: the next one is a week later). * * @param string $event Event. * @param int $now Unix time. */ private static function send( string $event, int $now ): void { if ( ! self::allowed() ) { return; } Api::post( '/api/v1/assistant/usage', self::payload( $event ), 5 ); $state = get_option( self::OPTION, array() ); $state = is_array( $state ) ? $state : array(); $state['last_sent'] = $now; update_option( self::OPTION, $state, false ); } /** * «Allow» / «No thanks» (notice) and «Turn on» / «Turn off» (Settings) — admin-post. */ public function handle(): void { if ( ! OwnKeySection::can_manage() ) { wp_die( esc_html__( 'You are not allowed to access this page.', 'addsynapse-store-assistant' ), 403 ); } check_admin_referer( self::ACTION ); $choice = isset( $_POST['addsynapse_assistant_usage'] ) ? sanitize_key( wp_unslash( $_POST['addsynapse_assistant_usage'] ) ) : ''; $tab = isset( $_POST['addsynapse_assistant_tab'] ) ? sanitize_key( wp_unslash( $_POST['addsynapse_assistant_tab'] ) ) : ''; if ( 'allow' === $choice ) { if ( ! self::allowed() ) { self::allow(); } } else { self::decline(); } wp_safe_redirect( AdminPage::back_url( $tab ) . ( 'settings' === $tab ? '#addsynapse-assistant-usage' : '' ) ); exit; } /** * The one-time notice on one of the plugin's own screens. * * @param string $tab chat | settings. */ public static function render( string $tab ): void { if ( ! self::eligible() ) { return; } echo '
'; echo '

' . esc_html__( 'Help improve Store Assistant', 'addsynapse-store-assistant' ) . ' '; echo esc_html__( 'Share anonymous usage info with AddSynapse, the makers of this plugin: the plugin, WordPress and WooCommerce versions, the site language and a random site ID — once now, then once a week. No content, no customer data, no email or site address. You can turn it off in Settings at any time.', 'addsynapse-store-assistant' ) . ' '; echo '' . esc_html__( 'Privacy', 'addsynapse-store-assistant' ) . '

'; self::render_form( $tab, 'allow', _x( 'Allow', 'anonymous usage info', 'addsynapse-store-assistant' ), 'decline', __( 'No thanks', 'addsynapse-store-assistant' ) ); echo '
'; } /** * Settings → «Anonymous usage info»: the state and the switch. */ public static function render_settings(): void { if ( ! OwnKeySection::can_manage() ) { return; } echo '

' . esc_html__( 'Anonymous usage info', 'addsynapse-store-assistant' ) . '

'; if ( self::allowed() ) { echo '

' . esc_html__( 'On. Once a week this site sends AddSynapse the plugin, WordPress and WooCommerce versions, the site language and a random site ID. No content, no customer data, no email or site address.', 'addsynapse-store-assistant' ) . '

'; self::render_form( 'settings', 'decline', __( 'Turn off', 'addsynapse-store-assistant' ) ); echo '

' . esc_html__( 'Turning it off stops the weekly call and deletes the random ID on this site.', 'addsynapse-store-assistant' ) . '

'; return; } echo '

' . esc_html__( 'Off. Nothing is sent. If you turn it on, this site sends AddSynapse once now and then once a week the plugin, WordPress and WooCommerce versions, the site language and a random site ID — no content, no customer data, no email or site address. It helps us know how many shops use Store Assistant.', 'addsynapse-store-assistant' ) . '

'; self::render_form( 'settings', 'allow', __( 'Turn on', 'addsynapse-store-assistant' ) ); } /** * Form with one or two buttons. * * @param string $tab Tab. * @param string $value Primary button value. * @param string $label Primary button label. * @param string $second_value Second button value. * @param string $second_label Second button label. */ private static function render_form( string $tab, string $value, string $label, string $second_value = '', string $second_label = '' ): void { echo '
'; wp_nonce_field( self::ACTION ); echo ''; echo ''; echo ''; if ( '' !== $second_value ) { echo ' '; } echo '
'; } }