self::VERSION, 'status' => 'pending', 'started_at' => null, 'completed_at' => null, 'dismissed_at' => null, ]; } /** * @return State */ public static function all(): array { $stored = get_option( self::OPTION, [] ); return self::coerce( is_array( $stored ) ? $stored : [] ); } public static function is_finished(): bool { return in_array( self::all()['status'], [ 'completed', 'dismissed' ], true ); } /** * Takes a status from the browser and stamps the time it first got there. * Going back to pending (the guide started again) clears the times. * * @param array $input * @return State */ public static function update( array $input ): array { $next = self::all(); $clean = self::sanitize( $input ); if ( ! isset( $clean['status'] ) ) { return $next; } if ( 'pending' === $clean['status'] ) { $next = self::defaults(); } $next['status'] = $clean['status']; $stamp = [ 'in_progress' => 'started_at', 'completed' => 'completed_at', 'dismissed' => 'dismissed_at', ][ $next['status'] ] ?? ''; if ( '' !== $stamp && null === $next[ $stamp ] ) { $next[ $stamp ] = time(); } update_option( self::OPTION, $next, false ); return self::coerce( $next ); } /** * A site that had a floor before the guide existed is set up already, so * it never sees the welcome. Runs once: after that the option is there, * and the floor is not even counted. * * @param callable():bool $has_floor */ public static function settle( callable $has_floor ): void { if ( null !== get_option( self::OPTION, null ) ) { return; } $state = self::defaults(); if ( $has_floor() ) { $state['status'] = 'completed'; $state['completed_at'] = time(); } add_option( self::OPTION, $state, '', false ); } /** * Only a known status gets through; times are never taken from a request. * * @param array $input * @return array{status?:string} */ public static function sanitize( array $input ): array { $status = $input['status'] ?? null; return is_string( $status ) && in_array( $status, self::STATUSES, true ) ? [ 'status' => $status ] : []; } /** * A stored value of the wrong shape still reads as a valid state. * * @param array $stored * @return State */ private static function coerce( array $stored ): array { $out = self::defaults(); if ( is_string( $stored['status'] ?? null ) && in_array( $stored['status'], self::STATUSES, true ) ) { $out['status'] = $stored['status']; } foreach ( [ 'started_at', 'completed_at', 'dismissed_at' ] as $key ) { if ( isset( $stored[ $key ] ) && is_numeric( $stored[ $key ] ) ) { $out[ $key ] = (int) $stored[ $key ]; } } return $out; } }