= self::MIN_SCORE; } /** * One real action: the owner changed something through the plugin. Pure — * the caller persists the result if it changed. * * @param array $state Stored state. * @return array Updated state. */ public static function acted( array $state ) { $state = self::shape( $state ); if ( $state['actions'] < self::VISIT_CAP ) { ++$state['actions']; } return $state; } /** * Whether a REST call counts as "really using the plugin": a state-changing * method on the plugin's own namespace — except answering the review ask * itself, which must not feed the gates it serves. Pure. * * @param string $route The dispatched route, e.g. '/agentimus/v1/settings'. * @param string $method HTTP method. * @return bool */ public static function use_worthy( $route, $method ) { if ( ! in_array( $method, array( 'POST', 'PUT', 'PATCH', 'DELETE' ), true ) ) { return false; } $ns = '/' . Rest::NAMESPACE . '/'; if ( 0 !== strpos( (string) $route, $ns ) ) { return false; } return $ns . 'review-ack' !== $route; } /** * Record the owner's answer. 'later' snoozes for a month; 'review' and * 'done' close the ask for good. An unknown answer changes nothing — a * malformed request must never close (or reopen) the ask. Pure. * * @param array $state Stored state. * @param string $answer 'review' | 'done' | 'later'. * @param int $now Current Unix time. * @return array Updated state. */ public static function answered( array $state, $answer, $now ) { $state = self::shape( $state ); if ( 'closed' === $state['state'] ) { return $state; // A final answer is final — a stale tab's click can't reopen the ask. } if ( 'later' === $answer ) { $state['state'] = 'later'; $state['until'] = (int) $now + self::SNOOZE_DAYS * DAY_IN_SECONDS; } elseif ( 'review' === $answer || 'done' === $answer ) { $state['state'] = 'closed'; $state['until'] = 0; } return $state; } /* -- Thin WordPress wrappers over the pure core. ----------------------- */ /** The stored state, normalised. */ public static function state() { return self::shape( (array) get_option( self::OPTION, array() ) ); } /** Count a plugin-screen load; writes only when something changed. */ public static function touch() { $before = self::state(); $after = self::touched( $before, time() ); if ( $after !== $before ) { update_option( self::OPTION, $after ); } } /** Count a real action; writes only when something changed. */ public static function used() { $before = self::state(); $after = self::acted( $before ); if ( $after !== $before ) { update_option( self::OPTION, $after ); } } /** * Persist an answer and return the resulting state string. * * @param string $answer 'review' | 'done' | 'later'. * @return string '' | 'later' | 'closed'. */ public static function ack( $answer ) { $state = self::answered( self::state(), (string) $answer, time() ); update_option( self::OPTION, $state ); return $state['state']; } /** Whether the ask has been answered for good (footer softening reads this). */ public static function closed() { $state = self::state(); return 'closed' === $state['state']; } /** * Normalise any stored value to the canonical shape — unknown keys drop, * missing ones default, and a corrupt state string becomes "unanswered". * * @param array $state Raw stored value. * @return array{since:int,visits:int,state:string,until:int} */ private static function shape( array $state ) { $status = isset( $state['state'] ) ? (string) $state['state'] : ''; return array( 'since' => isset( $state['since'] ) ? (int) $state['since'] : 0, 'visits' => isset( $state['visits'] ) ? (int) $state['visits'] : 0, 'actions' => isset( $state['actions'] ) ? (int) $state['actions'] : 0, 'state' => in_array( $status, array( '', 'later', 'closed' ), true ) ? $status : '', 'until' => isset( $state['until'] ) ? (int) $state['until'] : 0, ); } }