← All changes
|
includes/core/rest-api/Controllers/class-storecheckout-controller.php
+649
-0
3.11.1
→
3.13.3
View file →
| @@ -111,8 +111,58 @@ | ||
| 111 | 111 | ), |
| 112 | 112 | ) |
| 113 | 113 | ); |
| 114 | 114 | |
| 115 | + // One page of the React Store Checkout screen (`#/store-checkout`). | |
| 116 | + register_rest_route( | |
| 117 | + $this->namespace, | |
| 118 | + '/' . $this->rest_base . '/listing', | |
| 119 | + array( | |
| 120 | + array( | |
| 121 | + 'methods' => WP_REST_Server::READABLE, | |
| 122 | + 'callback' => array( $this, 'get_listing' ), | |
| 123 | + 'permission_callback' => array( $this, 'get_items_permissions_check' ), | |
| 124 | + 'args' => array( | |
| 125 | + 'page' => array( | |
| 126 | + 'type' => 'integer', | |
| 127 | + 'default' => 1, | |
| 128 | + 'sanitize_callback' => 'absint', | |
| 129 | + ), | |
| 130 | + 'per_page' => array( | |
| 131 | + 'type' => 'integer', | |
| 132 | + 'default' => 25, | |
| 133 | + 'sanitize_callback' => 'absint', | |
| 134 | + ), | |
| 135 | + 'search' => array( | |
| 136 | + 'type' => 'string', | |
| 137 | + 'default' => '', | |
| 138 | + 'sanitize_callback' => 'sanitize_text_field', | |
| 139 | + ), | |
| 140 | + /* | |
| 141 | + * Trash is a value of this filter rather than a tab of | |
| 142 | + * its own, the way the React funnel list reads it — the | |
| 143 | + * legacy screen had a separate `sc_status=trash` view. | |
| 144 | + */ | |
| 145 | + 'funnel_status' => array( | |
| 146 | + 'type' => 'string', | |
| 147 | + 'default' => 'all', | |
| 148 | + 'enum' => array( 'all', 'publish', 'draft', 'trash' ), | |
| 149 | + ), | |
| 150 | + 'orderby' => array( | |
| 151 | + 'type' => 'string', | |
| 152 | + 'default' => 'date', | |
| 153 | + 'enum' => array( 'date', 'title', 'modified' ), | |
| 154 | + ), | |
| 155 | + 'order' => array( | |
| 156 | + 'type' => 'string', | |
| 157 | + 'default' => 'desc', | |
| 158 | + 'enum' => array( 'asc', 'desc' ), | |
| 159 | + ), | |
| 160 | + ), | |
| 161 | + ), | |
| 162 | + ) | |
| 163 | + ); | |
| 164 | + | |
| 115 | 165 | // Validate Store Checkout creation (always allowed in multi-checkout mode) |
| 116 | 166 | register_rest_route( |
| 117 | 167 | $this->namespace, |
| 118 | 168 | '/' . $this->rest_base . '/validate-creation', |
| @@ -534,8 +584,607 @@ | ||
| 534 | 584 | return rest_ensure_response( array( |
| 535 | 585 | 'success' => true, |
| 536 | 586 | 'items' => $results, |
| 537 | 587 | ) ); |
| 588 | + } | |
| 589 | + | |
| 590 | + | |
| 591 | + /** | |
| 592 | + * Free installs may keep this many store checkouts before the screen stops | |
| 593 | + * offering to build another. Same number the legacy view hard-coded. | |
| 594 | + */ | |
| 595 | + const FREE_STORE_CHECKOUT_LIMIT = 3; | |
| 596 | + | |
| 597 | + | |
| 598 | + /** | |
| 599 | + * One page of the React Store Checkout screen. | |
| 600 | + * | |
| 601 | + * Everything the screen needs travels with the listing — the analytics | |
| 602 | + * strip, the status counts and the free-plan allowance — because all three | |
| 603 | + * change with the rows and a second round trip would let them disagree with | |
| 604 | + * what is on screen. | |
| 605 | + * | |
| 606 | + * Priority is worked out over the whole set rather than the page: it is a | |
| 607 | + * funnel's position in the evaluation order of every store checkout on the | |
| 608 | + * site, so paging must not renumber it. | |
| 609 | + * | |
| 610 | + * @param WP_REST_Request $request Full data about the request. | |
| 611 | + * | |
| 612 | + * @return WP_REST_Response | |
| 613 | + * @since 3.13.6 | |
| 614 | + */ | |
| 615 | + public function get_listing( $request ) { | |
| 616 | + $this->ensure_conditions_class(); | |
| 617 | + | |
| 618 | + $page = max( 1, (int) $request->get_param( 'page' ) ); | |
| 619 | + $per_page = (int) $request->get_param( 'per_page' ); | |
| 620 | + $per_page = $per_page > 0 ? min( $per_page, 100 ) : 25; | |
| 621 | + $search = (string) $request->get_param( 'search' ); | |
| 622 | + $status = (string) $request->get_param( 'funnel_status' ); | |
| 623 | + $orderby = (string) $request->get_param( 'orderby' ); | |
| 624 | + $order = 'asc' === $request->get_param( 'order' ) ? 'ASC' : 'DESC'; | |
| 625 | + | |
| 626 | + $post_status = in_array( $status, array( 'publish', 'draft', 'trash' ), true ) | |
| 627 | + ? array( $status ) | |
| 628 | + : array( 'publish', 'draft', 'trash' ); | |
| 629 | + | |
| 630 | + $matching = $this->query_store_checkouts( $post_status, $search, $orderby, $order ); | |
| 631 | + $total = count( $matching ); | |
| 632 | + $posts = array_slice( $matching, ( $page - 1 ) * $per_page, $per_page ); | |
| 633 | + | |
| 634 | + /* | |
| 635 | + * The evaluation order is a property of the live set, not of the filter | |
| 636 | + * being looked at: a trashed checkout is evaluated by nobody, and a | |
| 637 | + * search must not move the numbers of the rows it happens to match. | |
| 638 | + */ | |
| 639 | + $priority = $this->get_priority_map(); | |
| 640 | + | |
| 641 | + $items = array(); | |
| 642 | + | |
| 643 | + foreach ( $posts as $funnel ) { | |
| 644 | + $items[] = $this->prepare_row( $funnel, $priority ); | |
| 645 | + } | |
| 646 | + | |
| 647 | + return rest_ensure_response( | |
| 648 | + array( | |
| 649 | + 'success' => true, | |
| 650 | + 'items' => $items, | |
| 651 | + 'total' => $total, | |
| 652 | + 'total_pages' => $per_page > 0 ? (int) ceil( $total / $per_page ) : 0, | |
| 653 | + 'page' => $page, | |
| 654 | + 'per_page' => $per_page, | |
| 655 | + 'counts' => $this->get_listing_counts(), | |
| 656 | + 'analytics' => $this->get_listing_analytics(), | |
| 657 | + 'meta' => $this->get_listing_meta(), | |
| 658 | + ) | |
| 659 | + ); | |
| 660 | + } | |
| 661 | + | |
| 662 | + | |
| 663 | + /** | |
| 664 | + * Every store checkout matching a status and a search, newest first. | |
| 665 | + * | |
| 666 | + * Resolved in full rather than paged by WP_Query because the screen also | |
| 667 | + * needs the whole set to number the evaluation order, and asking twice for | |
| 668 | + * the same posts is the more expensive of the two. | |
| 669 | + * | |
| 670 | + * @param array $post_status Post statuses to match. | |
| 671 | + * @param string $search Search term, or an empty string. | |
| 672 | + * @param string $orderby Order column. | |
| 673 | + * @param string $order `ASC` or `DESC`. | |
| 674 | + * | |
| 675 | + * @return \WP_Post[] | |
| 676 | + * @since 3.13.6 | |
| 677 | + */ | |
| 678 | + private function query_store_checkouts( $post_status, $search = '', $orderby = 'date', $order = 'DESC' ) { | |
| 679 | + $args = array( | |
| 680 | + 'post_type' => WPFNL_FUNNELS_POST_TYPE, | |
| 681 | + 'posts_per_page' => -1, | |
| 682 | + 'post_status' => $post_status, | |
| 683 | + 'orderby' => $orderby, | |
| 684 | + 'order' => $order, | |
| 685 | + 'no_found_rows' => true, | |
| 686 | + 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query | |
| 687 | + array( | |
| 688 | + 'key' => '_wpfnl_funnel_type', | |
| 689 | + 'value' => 'store_checkout', | |
| 690 | + ), | |
| 691 | + ), | |
| 692 | + ); | |
| 693 | + | |
| 694 | + if ( $search ) { | |
| 695 | + $args['s'] = $search; | |
| 696 | + } | |
| 697 | + | |
| 698 | + return get_posts( $args ); | |
| 699 | + } | |
| 700 | + | |
| 701 | + | |
| 702 | + /** | |
| 703 | + * Where each live store checkout sits in the evaluation order. | |
| 704 | + * | |
| 705 | + * The same ordering the matcher itself applies: conditional checkouts are | |
| 706 | + * tried newest first, and the catch-all is the fallback when none of them | |
| 707 | + * match. Only the newest catch-all can ever be reached — the ones behind it | |
| 708 | + * are overridden, and the screen says so rather than showing a number that | |
| 709 | + * means nothing. | |
| 710 | + * | |
| 711 | + * @return array `[ 'position' => int[], 'overridden' => int[], 'catchAll' => int[] ]`. | |
| 712 | + * @since 3.13.6 | |
| 713 | + */ | |
| 714 | + private function get_priority_map() { | |
| 715 | + $funnels = Wpfnl_Store_Checkout_Conditions::get_all_store_checkout_funnels(); | |
| 716 | + $conditional = array(); | |
| 717 | + $catch_all = array(); | |
| 718 | + | |
| 719 | + foreach ( $funnels as $funnel ) { | |
| 720 | + $step_id = Wpfnl_Store_Checkout_Conditions::get_checkout_step_id_for_funnel( $funnel->ID ); | |
| 721 | + $condition = $step_id | |
| 722 | + ? Wpfnl_Store_Checkout_Conditions::get_condition( $step_id ) | |
| 723 | + : array( 'condition_type' => 'all' ); | |
| 724 | + | |
| 725 | + if ( Wpfnl_Store_Checkout_Conditions::is_catch_all( $condition ) ) { | |
| 726 | + $catch_all[] = (int) $funnel->ID; | |
| 727 | + } else { | |
| 728 | + $conditional[] = (int) $funnel->ID; | |
| 729 | + } | |
| 730 | + } | |
| 731 | + | |
| 732 | + $position = array(); | |
| 733 | + $overridden = array(); | |
| 734 | + $next = 1; | |
| 735 | + | |
| 736 | + foreach ( $conditional as $funnel_id ) { | |
| 737 | + $position[ $funnel_id ] = $next++; | |
| 738 | + } | |
| 739 | + | |
| 740 | + foreach ( $catch_all as $index => $funnel_id ) { | |
| 741 | + $position[ $funnel_id ] = $next++; | |
| 742 | + | |
| 743 | + if ( $index > 0 ) { | |
| 744 | + $overridden[] = $funnel_id; | |
| 745 | + } | |
| 746 | + } | |
| 747 | + | |
| 748 | + return array( | |
| 749 | + 'position' => $position, | |
| 750 | + 'overridden' => $overridden, | |
| 751 | + 'catchAll' => $catch_all, | |
| 752 | + ); | |
| 753 | + } | |
| 754 | + | |
| 755 | + | |
| 756 | + /** | |
| 757 | + * Shape one row of the listing. | |
| 758 | + * | |
| 759 | + * @param \WP_Post $funnel Store checkout funnel. | |
| 760 | + * @param array $priority Output of get_priority_map(). | |
| 761 | + * | |
| 762 | + * @return array | |
| 763 | + * @since 3.13.6 | |
| 764 | + */ | |
| 765 | + private function prepare_row( $funnel, $priority ) { | |
| 766 | + $funnel_id = (int) $funnel->ID; | |
| 767 | + $step_id = Wpfnl_Store_Checkout_Conditions::get_checkout_step_id_for_funnel( $funnel_id ); | |
| 768 | + $condition = $step_id | |
| 769 | + ? Wpfnl_Store_Checkout_Conditions::get_condition( $step_id ) | |
| 770 | + : array( 'condition_type' => 'all' ); | |
| 771 | + | |
| 772 | + $is_trash = 'trash' === $funnel->post_status; | |
| 773 | + $badges = $this->get_condition_badges( $condition ); | |
| 774 | + | |
| 775 | + /* | |
| 776 | + * A catch-all has no badge of its own, so the fallback one is added | |
| 777 | + * here and the row is marked as having no real condition — which is | |
| 778 | + * also what decides the wording of its priority. | |
| 779 | + */ | |
| 780 | + $is_catch_all = empty( $badges ); | |
| 781 | + | |
| 782 | + if ( $is_catch_all ) { | |
| 783 | + $badges[] = array( | |
| 784 | + 'label' => __( 'No Condition', 'wpfnl' ), | |
| 785 | + 'tooltip' => __( 'This store checkout will apply as the default WooCommerce checkout without any condition.', 'wpfnl' ), | |
| 786 | + 'isDate' => false, | |
| 787 | + ); | |
| 788 | + } | |
| 789 | + | |
| 790 | + $summary = ''; | |
| 791 | + | |
| 792 | + foreach ( $badges as $index => $badge ) { | |
| 793 | + if ( $index > 0 ) { | |
| 794 | + $summary .= empty( $badge['isDate'] ) ? ' OR ' : ' AND '; | |
| 795 | + } | |
| 796 | + | |
| 797 | + $summary .= $badge['label']; | |
| 798 | + } | |
| 799 | + | |
| 800 | + $view_url = ''; | |
| 801 | + | |
| 802 | + if ( ! $is_trash && $step_id ) { | |
| 803 | + $permalink = get_the_permalink( $step_id ); | |
| 804 | + $view_url = $permalink ? esc_url_raw( $permalink ) : ''; | |
| 805 | + } | |
| 806 | + | |
| 807 | + return array( | |
| 808 | + 'id' => $funnel_id, | |
| 809 | + 'name' => $funnel->post_title ? $funnel->post_title : __( '(no title)', 'wpfnl' ), | |
| 810 | + 'status' => $funnel->post_status, | |
| 811 | + 'statusLabel' => $this->get_status_label( $funnel->post_status ), | |
| 812 | + 'builder' => Wpfnl_functions::get_page_builder_by_step_id( $funnel_id ), | |
| 813 | + 'stepId' => $step_id ? (int) $step_id : 0, | |
| 814 | + 'createdAt' => $funnel->post_date_gmt, | |
| 815 | + // Rendered server-side so the column reads in the site's own | |
| 816 | + // locale and date format rather than the browser's. | |
| 817 | + 'createdLabel' => get_the_date( 'Y-m-d g:i A', $funnel_id ), | |
| 818 | + 'conditionSummary' => $summary, | |
| 819 | + 'conditionTooltip' => implode( ' ', wp_list_pluck( $badges, 'tooltip' ) ), | |
| 820 | + 'hasCondition' => ! $is_catch_all, | |
| 821 | + 'editUrl' => $is_trash ? '' : $this->get_funnel_edit_url( $funnel_id ), | |
| 822 | + 'viewUrl' => $view_url, | |
| 823 | + ) + $this->get_row_priority( $funnel_id, $priority, $is_catch_all, $is_trash ); | |
| 824 | + } | |
| 825 | + | |
| 826 | + | |
| 827 | + /** | |
| 828 | + * The priority badge of one row: its wording, its tone and why. | |
| 829 | + * | |
| 830 | + * @param int $funnel_id Funnel id. | |
| 831 | + * @param array $priority Output of get_priority_map(). | |
| 832 | + * @param bool $is_catch_all Whether the checkout carries no condition. | |
| 833 | + * @param bool $is_trash Whether the checkout is in the trash. | |
| 834 | + * | |
| 835 | + * @return array | |
| 836 | + * @since 3.13.6 | |
| 837 | + */ | |
| 838 | + private function get_row_priority( $funnel_id, $priority, $is_catch_all, $is_trash ) { | |
| 839 | + // Nothing in the trash is ever evaluated, so it has no place in the order. | |
| 840 | + if ( $is_trash || ! isset( $priority['position'][ $funnel_id ] ) ) { | |
| 841 | + return array( | |
| 842 | + 'priorityLabel' => '—', | |
| 843 | + 'priorityTone' => 'none', | |
| 844 | + 'priorityTooltip' => __( 'Trashed store checkouts are never evaluated.', 'wpfnl' ), | |
| 845 | + ); | |
| 846 | + } | |
| 847 | + | |
| 848 | + if ( in_array( $funnel_id, $priority['overridden'], true ) ) { | |
| 849 | + return array( | |
| 850 | + 'priorityLabel' => __( 'Overridden', 'wpfnl' ), | |
| 851 | + 'priorityTone' => 'overridden', | |
| 852 | + 'priorityTooltip' => __( 'This fallback checkout is overridden by a newer fallback. Only the most recently created fallback is active when no conditional checkout matches.', 'wpfnl' ), | |
| 853 | + ); | |
| 854 | + } | |
| 855 | + | |
| 856 | + if ( $is_catch_all ) { | |
| 857 | + return array( | |
| 858 | + 'priorityLabel' => __( 'Default', 'wpfnl' ), | |
| 859 | + 'priorityTone' => 'fallback', | |
| 860 | + 'priorityTooltip' => __( 'This checkout has no condition and acts as the default fallback. It will only be used when no other conditional store checkout matches the customer\'s cart.', 'wpfnl' ), | |
| 861 | + ); | |
| 862 | + } | |
| 863 | + | |
| 864 | + $position = (int) $priority['position'][ $funnel_id ]; | |
| 865 | + | |
| 866 | + if ( 1 === $position ) { | |
| 867 | + return array( | |
| 868 | + 'priorityLabel' => __( 'Highest', 'wpfnl' ), | |
| 869 | + 'priorityTone' => 'active', | |
| 870 | + 'priorityTooltip' => __( 'This is the most recently created conditional checkout. It will be evaluated first and takes priority over all other store checkouts.', 'wpfnl' ), | |
| 871 | + ); | |
| 872 | + } | |
| 873 | + | |
| 874 | + return array( | |
| 875 | + /* translators: %d: priority position number. */ | |
| 876 | + 'priorityLabel' => sprintf( __( '#%d', 'wpfnl' ), $position ), | |
| 877 | + 'priorityTone' => 'active', | |
| 878 | + /* translators: %d: priority position number. */ | |
| 879 | + 'priorityTooltip' => sprintf( __( 'This checkout has evaluation priority #%d. Store checkouts with conditions are evaluated from newest to oldest. The first match wins.', 'wpfnl' ), $position ), | |
| 880 | + ); | |
| 881 | + } | |
| 882 | + | |
| 883 | + | |
| 884 | + /** | |
| 885 | + * One badge per rule a condition carries, plus the date range when it has | |
| 886 | + * one. An empty list means the checkout is a catch-all. | |
| 887 | + * | |
| 888 | + * @param array $condition Stored condition. | |
| 889 | + * | |
| 890 | + * @return array List of `[ 'label' => string, 'tooltip' => string, 'isDate' => bool ]`. | |
| 891 | + * @since 3.13.6 | |
| 892 | + */ | |
| 893 | + private function get_condition_badges( $condition ) { | |
| 894 | + $labels = array( | |
| 895 | + 'all' => __( 'No Condition', 'wpfnl' ), | |
| 896 | + 'products' => __( 'Specific Products', 'wpfnl' ), | |
| 897 | + 'categories' => __( 'Product Categories', 'wpfnl' ), | |
| 898 | + 'tags' => __( 'Product Tags', 'wpfnl' ), | |
| 899 | + 'date_range' => __( 'Date Range', 'wpfnl' ), | |
| 900 | + 'rules' => __( 'Custom Rules', 'wpfnl' ), | |
| 901 | + ); | |
| 902 | + | |
| 903 | + $type = isset( $condition['condition_type'] ) ? $condition['condition_type'] : 'all'; | |
| 904 | + $badges = array(); | |
| 905 | + $rules = array(); | |
| 906 | + | |
| 907 | + if ( 'rules' === $type && ! empty( $condition['rules'] ) && is_array( $condition['rules'] ) ) { | |
| 908 | + $rules = $condition['rules']; | |
| 909 | + | |
| 910 | + foreach ( $rules as $rule ) { | |
| 911 | + $rule_type = isset( $rule['type'] ) ? $rule['type'] : 'all'; | |
| 912 | + | |
| 913 | + $badges[] = array( | |
| 914 | + 'label' => isset( $labels[ $rule_type ] ) ? $labels[ $rule_type ] : $labels['all'], | |
| 915 | + 'tooltip' => $this->get_rule_tooltip( $rule, count( $rules ) ), | |
| 916 | + 'isDate' => false, | |
| 917 | + ); | |
| 918 | + } | |
| 919 | + } | |
| 920 | + | |
| 921 | + if ( empty( $condition['use_date_range'] ) ) { | |
| 922 | + return $badges; | |
| 923 | + } | |
| 924 | + | |
| 925 | + $badges[] = array( | |
| 926 | + 'label' => $labels['date_range'], | |
| 927 | + 'tooltip' => $this->get_date_tooltip( $condition, ! empty( $rules ) ), | |
| 928 | + 'isDate' => true, | |
| 929 | + ); | |
| 930 | + | |
| 931 | + return $badges; | |
| 932 | + } | |
| 933 | + | |
| 934 | + | |
| 935 | + /** | |
| 936 | + * What one rule means, with its product, category or tag ids resolved to | |
| 937 | + * the names the reader gave them. | |
| 938 | + * | |
| 939 | + * @param array $rule One rule of a condition. | |
| 940 | + * @param int $total_rules How many rules the condition carries. | |
| 941 | + * | |
| 942 | + * @return string | |
| 943 | + * @since 3.13.6 | |
| 944 | + */ | |
| 945 | + private function get_rule_tooltip( $rule, $total_rules = 1 ) { | |
| 946 | + $type = isset( $rule['type'] ) ? $rule['type'] : 'all'; | |
| 947 | + $values = isset( $rule['values'] ) ? (array) $rule['values'] : array(); | |
| 948 | + $or_note = $total_rules > 1 ? ' ' . __( '(OR with other rules)', 'wpfnl' ) : ''; | |
| 949 | + | |
| 950 | + if ( ! $values ) { | |
| 951 | + return __( 'This checkout will appear when the condition is met.', 'wpfnl' ) . $or_note; | |
| 952 | + } | |
| 953 | + | |
| 954 | + $names = array(); | |
| 955 | + | |
| 956 | + foreach ( $values as $value_id ) { | |
| 957 | + $value_id = (int) $value_id; | |
| 958 | + | |
| 959 | + if ( 'products' === $type ) { | |
| 960 | + $names[] = get_the_title( $value_id ); | |
| 961 | + continue; | |
| 962 | + } | |
| 963 | + | |
| 964 | + $taxonomy = 'categories' === $type ? 'product_cat' : ( 'tags' === $type ? 'product_tag' : '' ); | |
| 965 | + | |
| 966 | + if ( ! $taxonomy ) { | |
| 967 | + continue; | |
| 968 | + } | |
| 969 | + | |
| 970 | + $term = get_term( $value_id, $taxonomy ); | |
| 971 | + $names[] = ( $term && ! is_wp_error( $term ) ) ? $term->name : '#' . $value_id; | |
| 972 | + } | |
| 973 | + | |
| 974 | + $names = implode( ', ', $names ); | |
| 975 | + | |
| 976 | + switch ( $type ) { | |
| 977 | + case 'products': | |
| 978 | + /* translators: %s: comma separated product names. */ | |
| 979 | + return sprintf( __( 'This checkout will appear when the cart contains: %s.', 'wpfnl' ), $names ) . $or_note; | |
| 980 | + | |
| 981 | + case 'categories': | |
| 982 | + /* translators: %s: comma separated category names. */ | |
| 983 | + return sprintf( __( 'This checkout will appear when the cart has products from: %s.', 'wpfnl' ), $names ) . $or_note; | |
| 984 | + | |
| 985 | + case 'tags': | |
| 986 | + /* translators: %s: comma separated tag names. */ | |
| 987 | + return sprintf( __( 'This checkout will appear when the cart has products tagged: %s.', 'wpfnl' ), $names ) . $or_note; | |
| 988 | + } | |
| 989 | + | |
| 990 | + return __( 'This checkout will appear when the condition is met.', 'wpfnl' ) . $or_note; | |
| 991 | + } | |
| 992 | + | |
| 993 | + | |
| 994 | + /** | |
| 995 | + * What the date range of a condition means. | |
| 996 | + * | |
| 997 | + * A date range narrows whatever the rules already matched rather than | |
| 998 | + * offering another way to match, so its wording says "additionally" when | |
| 999 | + * there are rules alongside it. | |
| 1000 | + * | |
| 1001 | + * @param array $condition Stored condition. | |
| 1002 | + * @param bool $has_rules Whether the condition also carries rules. | |
| 1003 | + * | |
| 1004 | + * @return string | |
| 1005 | + * @since 3.13.6 | |
| 1006 | + */ | |
| 1007 | + private function get_date_tooltip( $condition, $has_rules ) { | |
| 1008 | + $from = isset( $condition['date_from'] ) ? $condition['date_from'] : ''; | |
| 1009 | + $to = isset( $condition['date_to'] ) ? $condition['date_to'] : ''; | |
| 1010 | + | |
| 1011 | + if ( $from && $to ) { | |
| 1012 | + return $has_rules | |
| 1013 | + /* translators: 1: start date, 2: end date. */ | |
| 1014 | + ? sprintf( __( 'Additionally, this checkout will only be active between %1$s and %2$s.', 'wpfnl' ), $from, $to ) | |
| 1015 | + /* translators: 1: start date, 2: end date. */ | |
| 1016 | + : sprintf( __( 'This checkout will only be active between %1$s and %2$s.', 'wpfnl' ), $from, $to ); | |
| 1017 | + } | |
| 1018 | + | |
| 1019 | + if ( $from ) { | |
| 1020 | + return $has_rules | |
| 1021 | + /* translators: %s: start date. */ | |
| 1022 | + ? sprintf( __( 'Additionally, this checkout will only be active from %s onwards.', 'wpfnl' ), $from ) | |
| 1023 | + /* translators: %s: start date. */ | |
| 1024 | + : sprintf( __( 'This checkout will only be active from %s onwards.', 'wpfnl' ), $from ); | |
| 1025 | + } | |
| 1026 | + | |
| 1027 | + if ( $to ) { | |
| 1028 | + return $has_rules | |
| 1029 | + /* translators: %s: end date. */ | |
| 1030 | + ? sprintf( __( 'Additionally, this checkout will only be active until %s.', 'wpfnl' ), $to ) | |
| 1031 | + /* translators: %s: end date. */ | |
| 1032 | + : sprintf( __( 'This checkout will only be active until %s.', 'wpfnl' ), $to ); | |
| 1033 | + } | |
| 1034 | + | |
| 1035 | + return __( 'This checkout has a date range condition.', 'wpfnl' ); | |
| 1036 | + } | |
| 1037 | + | |
| 1038 | + | |
| 1039 | + /** | |
| 1040 | + * Orders, revenue, average order value and bump revenue across every live | |
| 1041 | + * store checkout. | |
| 1042 | + * | |
| 1043 | + * @return array | |
| 1044 | + * @since 3.13.6 | |
| 1045 | + */ | |
| 1046 | + private function get_listing_analytics() { | |
| 1047 | + global $wpdb; | |
| 1048 | + | |
| 1049 | + $analytics = array( | |
| 1050 | + 'totalOrders' => 0, | |
| 1051 | + 'totalRevenue' => 0, | |
| 1052 | + 'aov' => 0, | |
| 1053 | + 'orderBumpRevenue' => 0, | |
| 1054 | + ); | |
| 1055 | + | |
| 1056 | + $ids = wp_list_pluck( Wpfnl_Store_Checkout_Conditions::get_all_store_checkout_funnels(), 'ID' ); | |
| 1057 | + $ids = array_filter( array_map( 'absint', (array) $ids ) ); | |
| 1058 | + | |
| 1059 | + if ( ! $ids ) { | |
| 1060 | + return $analytics; | |
| 1061 | + } | |
| 1062 | + | |
| 1063 | + $stats_table = $wpdb->prefix . 'wpfnl_stats'; | |
| 1064 | + | |
| 1065 | + // A site that has never tracked an order has no stats table, and that | |
| 1066 | + // is not an error worth failing the whole screen over. | |
| 1067 | + if ( ! $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $stats_table ) ) ) { // phpcs:ignore | |
| 1068 | + return $analytics; | |
| 1069 | + } | |
| 1070 | + | |
| 1071 | + // Safe to interpolate: every id has been through absint(). | |
| 1072 | + $placeholders = implode( ',', $ids ); | |
| 1073 | + | |
| 1074 | + $row = $wpdb->get_row( | |
| 1075 | + $wpdb->prepare( | |
| 1076 | + "SELECT COUNT(DISTINCT id) AS total_orders, | |
| 1077 | + COALESCE(SUM(total_sales), 0) AS total_revenue, | |
| 1078 | + COALESCE(SUM(orderbump_sales), 0) AS orderbump_revenue | |
| 1079 | + FROM {$stats_table} | |
| 1080 | + WHERE funnel_id IN ({$placeholders}) AND status IN (%s, %s)", | |
| 1081 | + 'completed', | |
| 1082 | + 'processing' | |
| 1083 | + ) | |
| 1084 | + ); // phpcs:ignore | |
| 1085 | + | |
| 1086 | + if ( ! $row ) { | |
| 1087 | + return $analytics; | |
| 1088 | + } | |
| 1089 | + | |
| 1090 | + $analytics['totalOrders'] = (int) $row->total_orders; | |
| 1091 | + $analytics['totalRevenue'] = round( (float) $row->total_revenue, 2 ); | |
| 1092 | + $analytics['orderBumpRevenue'] = round( (float) $row->orderbump_revenue, 2 ); | |
| 1093 | + $analytics['aov'] = $analytics['totalOrders'] > 0 | |
| 1094 | + ? round( $analytics['totalRevenue'] / $analytics['totalOrders'], 2 ) | |
| 1095 | + : 0; | |
| 1096 | + | |
| 1097 | + return $analytics; | |
| 1098 | + } | |
| 1099 | + | |
| 1100 | + | |
| 1101 | + /** | |
| 1102 | + * How many store checkouts each value of the status filter would find. | |
| 1103 | + * | |
| 1104 | + * @return array | |
| 1105 | + * @since 3.13.6 | |
| 1106 | + */ | |
| 1107 | + private function get_listing_counts() { | |
| 1108 | + $publish = count( $this->query_store_checkouts( array( 'publish' ) ) ); | |
| 1109 | + $draft = count( $this->query_store_checkouts( array( 'draft' ) ) ); | |
| 1110 | + $trash = count( $this->query_store_checkouts( array( 'trash' ) ) ); | |
| 1111 | + | |
| 1112 | + return array( | |
| 1113 | + 'all' => $publish + $draft + $trash, | |
| 1114 | + 'publish' => $publish, | |
| 1115 | + 'draft' => $draft, | |
| 1116 | + 'trash' => $trash, | |
| 1117 | + ); | |
| 1118 | + } | |
| 1119 | + | |
| 1120 | + | |
| 1121 | + /** | |
| 1122 | + * State the screen's chrome depends on: the plan, the store checkout | |
| 1123 | + * allowance and where the creation journey starts. | |
| 1124 | + * | |
| 1125 | + * @return array | |
| 1126 | + * @since 3.13.6 | |
| 1127 | + */ | |
| 1128 | + private function get_listing_meta() { | |
| 1129 | + $is_pro_active = (bool) apply_filters( 'wpfunnels/is_pro_license_activated', false ); | |
| 1130 | + $count = (int) Wpfnl_functions::count_store_checkout_funnels(); | |
| 1131 | + | |
| 1132 | + return array( | |
| 1133 | + 'isProActive' => $is_pro_active, | |
| 1134 | + 'isWooActive' => Wpfnl_functions::is_wc_active(), | |
| 1135 | + 'storeCheckoutCount' => $count, | |
| 1136 | + 'maxStoreCheckouts' => $is_pro_active ? -1 : self::FREE_STORE_CHECKOUT_LIMIT, | |
| 1137 | + 'limitReached' => ! $is_pro_active && $count >= self::FREE_STORE_CHECKOUT_LIMIT, | |
| 1138 | + 'upgradeUrl' => 'https://getwpfunnels.com/pricing/', | |
| 1139 | + ); | |
| 1140 | + } | |
| 1141 | + | |
| 1142 | + | |
| 1143 | + /** | |
| 1144 | + * Word for a post status, in this screen's terms. | |
| 1145 | + * | |
| 1146 | + * A store checkout is either taking over the WooCommerce checkout or it is | |
| 1147 | + * not, so published and draft read as Enabled and Disabled here — which is | |
| 1148 | + * what the legacy screen called them. | |
| 1149 | + * | |
| 1150 | + * @param string $status Post status. | |
| 1151 | + * | |
| 1152 | + * @return string | |
| 1153 | + * @since 3.13.6 | |
| 1154 | + */ | |
| 1155 | + private function get_status_label( $status ) { | |
| 1156 | + if ( 'publish' === $status ) { | |
| 1157 | + return __( 'Enabled', 'wpfnl' ); | |
| 1158 | + } | |
| 1159 | + | |
| 1160 | + if ( 'trash' === $status ) { | |
| 1161 | + return __( 'Trashed', 'wpfnl' ); | |
| 1162 | + } | |
| 1163 | + | |
| 1164 | + return __( 'Disabled', 'wpfnl' ); | |
| 1165 | + } | |
| 1166 | + | |
| 1167 | + | |
| 1168 | + /** | |
| 1169 | + * Canvas URL of a store checkout funnel. | |
| 1170 | + * | |
| 1171 | + * @param int $funnel_id Funnel id. | |
| 1172 | + * | |
| 1173 | + * @return string | |
| 1174 | + * @since 3.13.6 | |
| 1175 | + */ | |
| 1176 | + private function get_funnel_edit_url( $funnel_id ) { | |
| 1177 | + return esc_url_raw( | |
| 1178 | + add_query_arg( | |
| 1179 | + array( | |
| 1180 | + 'page' => WPFNL_EDIT_FUNNEL_SLUG, | |
| 1181 | + 'id' => $funnel_id, | |
| 1182 | + 'step_id' => 0, | |
| 1183 | + ), | |
| 1184 | + admin_url( 'admin.php' ) | |
| 1185 | + ) | |
| 1186 | + ); | |
| 538 | 1187 | } |
| 539 | 1188 | |
| 540 | 1189 | |
| 541 | 1190 | /** |