last_checked ?? null ) || $updates->last_checked > time() || $updates->last_checked < time() - 2 * DAY_IN_SECONDS ) { return false; } foreach ( array( 'response', 'no_update' ) as $bucket ) { $entries = $updates->$bucket ?? array(); $entry = is_array( $entries ) ? ( $entries['orderworth/orderworth.php'] ?? null ) : null; if ( ! is_object( $entry ) || 'orderworth' !== ( $entry->slug ?? '' ) || 'w.org/plugins/orderworth' !== ( $entry->id ?? '' ) || 'https://wordpress.org/plugins/orderworth/' !== ( $entry->url ?? '' ) ) { continue; } $package = is_string( $entry->package ?? null ) ? wp_parse_url( $entry->package ) : false; if ( is_array( $package ) && 'https' === ( $package['scheme'] ?? '' ) && 'downloads.wordpress.org' === ( $package['host'] ?? '' ) && ! isset( $package['user'] ) && ! isset( $package['pass'] ) && ! isset( $package['port'] ) && ! isset( $package['query'] ) && ! isset( $package['fragment'] ) && preg_match( '~^/plugin/orderworth(?:\.[0-9]+(?:\.[0-9]+)*(?:[-.][a-zA-Z0-9]+)*)?\.zip$~D', $package['path'] ?? '' ) ) { return true; } } return false; } public static function review_url(): string { return 'https://wordpress.org/support/plugin/orderworth/reviews/#new-post'; } private static function normalize( $raw, int $now ): array { $raw = is_array( $raw ) ? $raw : array(); $first = is_int( $raw['first'] ?? null ) && $raw['first'] > 0 && $raw['first'] <= $now ? $raw['first'] : 0; $days = array(); foreach ( is_array( $raw['days'] ?? null ) ? $raw['days'] : array() as $day ) { if ( $first && is_string( $day ) && preg_match( '/^(\d{4})-(\d{2})-(\d{2})$/D', $day, $parts ) && checkdate( (int) $parts[2], (int) $parts[3], (int) $parts[1] ) && $day >= gmdate( 'Y-m-d', $first ) && $day <= gmdate( 'Y-m-d', $now ) ) { $days[] = $day; } } $days = array_values( array_unique( $days ) ); sort( $days ); $status = is_string( $raw['status'] ?? null ) && in_array( $raw['status'], array( 'waiting', 'snoozed', 'never', 'visited' ), true ) ? $raw['status'] : 'waiting'; return array( 'first' => $first, 'days' => array_slice( $days, 0, 3 ), 'status' => $status, 'until' => is_int( $raw['until'] ?? null ) && $raw['until'] > 0 ? $raw['until'] : 0 ); } public static function state(): array { return self::normalize( get_user_meta( get_current_user_id(), OWWC_User_State::key( self::META ), true ), time() ); } private static function revision( array $state ): string { return hash( 'sha256', wp_json_encode( $state ) ); } private static function eligible( array $state, int $now ): bool { return $state['first'] > 0 && $state['first'] <= $now - 30 * DAY_IN_SECONDS && count( $state['days'] ) >= 3 && ! in_array( $state['status'], array( 'never', 'visited' ), true ) && $state['until'] <= $now; } /** Serialized with dismissal so a late completed report cannot restore a prompt. */ private static function change( callable $change ): bool { $user_id = get_current_user_id(); $key = 'owwc_review_request_lock_' . $user_id; $lock = OWWC_Lock::acquire( $key, 30 ); if ( ! $lock ) { return false; } try { wp_cache_delete( $user_id, 'user_meta' ); $meta = OWWC_User_State::key( self::META ); $previous = get_user_meta( $user_id, $meta, true ); $next = $change( self::normalize( $previous, time() ) ); if ( ! is_array( $next ) ) { return false; } if ( $next === $previous ) { return true; } $saved = is_array( $previous ) ? update_user_meta( $user_id, $meta, $next, $previous ) : add_user_meta( $user_id, $meta, $next, true ); return (bool) $saved; } finally { OWWC_Lock::release( $key, $lock ); } } /** Called only on a server-verified scan's first complete transition. */ public static function report_completed( array $scan ): void { if ( ! get_current_user_id() || ! current_user_can( 'manage_woocommerce' ) || get_current_user_id() !== ( $scan['owner'] ?? 0 ) || ( 'complete' !== ( $scan['phase'] ?? '' ) && empty( $scan['guide_recorded'] ) ) || empty( $scan['groups'] ) || ! is_array( $scan['groups'] ) ) { return; } $has_orders = false; foreach ( $scan['groups'] as $group ) { if ( is_array( $group ) && ( ( is_int( $group['selected'] ?? null ) && $group['selected'] > 0 ) || ( is_array( $group['ids'] ?? null ) && count( $group['ids'] ) > 0 ) ) ) { $has_orders = true; } } if ( ! $has_orders ) { return; } self::change( static function ( array $state ): array { if ( in_array( $state['status'], array( 'never', 'visited' ), true ) ) { return $state; } if ( ! $state['first'] ) { $state['first'] = time(); } $today = gmdate( 'Y-m-d' ); if ( count( $state['days'] ) < 3 && ! in_array( $today, $state['days'], true ) ) { $state['days'][] = $today; } return $state; } ); } public static function assets( string $hook ): void { if ( 'toplevel_page_orderworth' === $hook && current_user_can( 'manage_woocommerce' ) ) { wp_enqueue_style( 'owwc-review-request', plugins_url( 'assets/review-request.css', OWWC_FILE ), array( 'owwc-admin' ), OWWC_VERSION ); } } private static function form( bool $prompt, bool $ready ): void { echo '
'; } public static function prompt( string $view ): void { if ( '' !== $view || ! current_user_can( 'manage_woocommerce' ) || ! self::directory_ready() || ! self::eligible( self::state(), time() ) ) { return; } echo '' . esc_html__( 'An honest review on WordPress.org helps other store owners decide whether OrderWorth fits their work. All feedback is welcome.', 'orderworth' ) . '
'; self::form( true, true ); echo '' . esc_html__( 'A review reminder can appear only inside OrderWorth after 30 days from your first completed, nonempty report and reports on three separate days. We keep only that first date, up to three report dates and your reminder preference in your user profile on this site. These are separate from optional workflow counts and are never sent to OrderWorth.', 'orderworth' ) . '
' . esc_html__( 'Choosing to review stops reminders; it does not tell us whether you posted a review. Dismissing the reminder stops further report-date collection for it. You can change this preference here.', 'orderworth' ) . '
'; if ( in_array( $state['status'], array( 'never', 'visited' ), true ) ) { echo '' . esc_html__( 'Review reminders are off.', 'orderworth' ) . '
'; } elseif ( $state['until'] > time() ) { /* translators: %s: postponed reminder date in UTC, formatted YYYY-MM-DD. */ echo '' . esc_html( sprintf( __( 'Review reminders are paused until %s (UTC).', 'orderworth' ), gmdate( 'Y-m-d', $state['until'] ) ) ) . '
'; } else { echo '' . esc_html__( 'A review reminder is allowed once the conditions above are met.', 'orderworth' ) . '
'; } if ( ! $ready ) { echo '' . esc_html__( 'The review link will be available once OrderWorth’s WordPress.org listing is public.', 'orderworth' ) . '
'; } self::form( false, $ready ); echo '