review_due() ) { $this->print_notice( 'dashicons-star-filled', __( 'Enjoying Abogo Booking? Help us with a 5-star review!', 'abogo-booking' ), __( 'It takes less than a minute, helps other shop owners find the plugin, and keeps us improving it. Thank you!', 'abogo-booking' ), array( 'review' => __( 'Leave a 5-star review', 'abogo-booking' ), 'later' => __( 'Later', 'abogo-booking' ), 'done' => __( 'Already done', 'abogo-booking' ), 'no' => _x( 'No', 'refuses the review request', 'abogo-booking' ), ), 'abfw_review_action' ); return; } if ( $this->trial_due() ) { $this->print_notice( 'dashicons-unlock', Locked::trial_offer(), __( 'Never sell the same dates twice, close dates, charge by date and by length of stay, and take bookings by telephone. Your bookings stay as they are, and nothing changes if you stop.', 'abogo-booking' ), array( 'trial' => __( 'Start the free trial', 'abogo-booking' ), 'snooze' => __( 'Not now', 'abogo-booking' ), 'never' => __( "Don't show again", 'abogo-booking' ), ), 'abfw_trial_action' ); } } /** * Answer the buttons of both requests. * * @since 1.1.0 */ public function handle() { // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Checked just below, once the request is ours. $review = isset( $_GET['abfw_review_action'] ) ? sanitize_key( wp_unslash( $_GET['abfw_review_action'] ) ) : ''; $trial = isset( $_GET['abfw_trial_action'] ) ? sanitize_key( wp_unslash( $_GET['abfw_trial_action'] ) ) : ''; // phpcs:enable WordPress.Security.NonceVerification.Recommended if ( '' === $review && '' === $trial ) { return; } check_admin_referer( 'abfw_notice' ); if ( '' !== $review && is_user_logged_in() ) { $this->answer_review( $review ); } if ( '' !== $trial && current_user_can( 'manage_options' ) ) { $this->answer_trial( $trial ); } wp_safe_redirect( remove_query_arg( array( 'abfw_review_action', 'abfw_trial_action', '_wpnonce' ) ) ); exit; } /** * A newly paid order may be the first paid booking, so the next page looks again. * * @since 1.1.0 */ public function recheck_review() { delete_transient( self::REVIEW_CHECKED ); } /** * Our own trial offer replaces the licensing service's, so the shop owner is * never asked twice and keeps the choice to put it off or refuse it. * * @since 1.1.0 */ public static function replace_licensing_trial_offer() { $licensing = function_exists( 'abfw_fs' ) ? \abfw_fs() : null; if ( ! $licensing ) { return; } $licensing->add_filter( 'show_trial', '__return_false' ); if ( $licensing->is_in_trial_promotion() ) { $licensing->remove_sticky( 'trial_promotion' ); } } /** * Print one request, with its buttons. * * @since 1.1.0 * @param string $icon Icon beside the request. * @param string $title What is asked. * @param string $text Why it is worth it. * @param array $buttons Label of each answer, by answer; the first is the main one. * @param string $field Name the answers are sent under. */ private function print_notice( $icon, $title, $text, array $buttons, $field ) { echo '
'; echo '

' . esc_html( $title ) . '

'; echo '

' . esc_html( $text ) . '

'; $first = true; foreach ( $buttons as $answer => $label ) { $url = wp_nonce_url( add_query_arg( $field, $answer ), 'abfw_notice' ); if ( 'trial' === $answer ) { $url = Locked::trial_url(); } printf( '%s ', esc_url( $url ), $first ? 'button button-primary' : 'button-link', esc_html( $label ) ); $first = false; } echo '

'; } /** * Remember the answer to the review request. * * @since 1.1.0 * @param string $answer The button clicked. */ private function answer_review( $answer ) { // Whatever the answer, the trial offer waits a few days rather than asking right after. update_option( self::TRIAL_SNOOZE, max( (int) get_option( self::TRIAL_SNOOZE, 0 ), time() + self::TRIAL_PAUSE_AFTER_REVIEW ), false ); if ( 'later' === $answer ) { update_option( self::REVIEW_SNOOZE, time() + 14 * DAY_IN_SECONDS, false ); return; } update_option( self::REVIEW_DONE, 'yes', false ); if ( 'review' === $answer ) { // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect -- WordPress.org is not a local address. wp_redirect( 'https://wordpress.org/support/plugin/abogo-booking/reviews/?filter=5#new-post' ); exit; } } /** * Remember the answer to the trial offer. * * @since 1.1.0 * @param string $answer The button clicked. */ private function answer_trial( $answer ) { if ( 'never' === $answer ) { update_option( self::TRIAL_DISMISSED, 'yes', false ); } if ( 'snooze' === $answer ) { update_option( self::TRIAL_SNOOZE, time() + 2 * WEEK_IN_SECONDS, false ); } } /** * A shop that has been paid for a booking knows whether the plugin works, so * it is asked wherever it is in the admin. * * @since 1.1.0 * @return bool */ private function review_due() { if ( ! is_user_logged_in() || 'yes' === get_option( self::REVIEW_DONE ) ) { return false; } if ( time() < (int) get_option( self::REVIEW_SNOOZE, 0 ) ) { return false; } if ( 'yes' === get_option( self::REVIEW_EARNED ) ) { return true; } if ( get_transient( self::REVIEW_CHECKED ) ) { return false; } if ( ! $this->has_paid_booking() ) { set_transient( self::REVIEW_CHECKED, 'yes', DAY_IN_SECONDS ); return false; } update_option( self::REVIEW_EARNED, 'yes', false ); return true; } /** * Whether one of the latest bookings comes from a paid order. * * @since 1.1.0 * @return bool */ private function has_paid_booking() { foreach ( Repository::query() as $booking ) { $order = $booking->order_id ? wc_get_order( $booking->order_id ) : false; if ( 'cancelled' !== $booking->status && $order && $order->is_paid() ) { return true; } } return false; } /** * Whether the trial is offered: to administrators of a free install that can * still start it, and never on a page already leading with it. * * @since 1.1.0 * @return bool */ private function trial_due() { if ( ! current_user_can( 'manage_options' ) || '' === Locked::trial_url() || 'yes' === get_option( self::TRIAL_DISMISSED ) ) { return false; } if ( time() < (int) get_option( self::TRIAL_SNOOZE, 0 ) ) { return false; } $screen = get_current_screen(); return ! $screen || false === strpos( $screen->id, 'abogo_booking-pricing' ); } }