PluginProbe ʕ •ᴥ•ʔ
WooCommerce / 10.9.3
WooCommerce v10.9.3
10.9.4 10.9.3 10.9.2 10.9.1 10.9.0 10.9.0-rc.1 10.9.0-beta.2 10.9.0-beta.1 10.8.1 10.8.0 10.8.0-rc.1 10.8.0-beta.2 10.8.0-beta.1 7.8.0-beta.1 7.8.0-beta.2 7.8.0-rc.1 7.8.0-rc.2 7.8.1 7.8.2 7.8.3 7.8.4 7.9.0 7.9.0-beta.1 7.9.0-beta.2 7.9.0-rc.2 7.9.0-rc.3 7.9.1 7.9.2 8.0.0 8.0.0-beta.1 8.0.0-beta.2 8.0.0-rc.1 8.0.0-rc.2 8.0.1 8.0.2 8.0.3 8.0.4 8.0.5 8.1.0 8.1.0-beta.1 8.1.0-rc.1 8.1.0-rc.2 8.1.1 8.1.2 8.1.3 8.1.4 8.2.0 8.2.0-beta.1 8.2.0-rc.1 8.2.0-rc.2 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.3.0 8.3.0-beta.1 8.3.0-rc.1 8.3.0-rc.2 8.3.1 8.3.2 8.3.3 8.3.4 8.4.0 8.4.0-beta.1 8.4.0-rc.1 8.4.1 8.4.2 8.4.3 8.5.0 8.5.0-beta.1 8.5.0-rc.1 8.5.1 8.5.2 8.5.3 8.5.4 8.5.5 8.6.0 8.6.0-beta.1 8.6.0-rc.1 8.6.1 8.6.2 8.6.3 8.6.4 8.7.0 8.7.0-beta.1 8.7.0-beta.2 8.7.0-rc.1 8.7.1 8.7.2 8.7.3 8.8.0 8.8.0-beta.1 8.8.0-rc.1 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.8.6 8.8.7 8.9.0 8.9.0-beta.1 8.9.0-rc.1 8.9.1 8.9.2 8.9.3 8.9.4 8.9.5 9.0.0 9.0.0-beta.1 9.0.0-beta.2 9.0.0-rc.1 9.0.1 9.0.2 9.0.3 9.0.4 9.1.0 9.1.0-beta.1 9.1.0-rc.1 9.1.1 9.1.2 9.1.3 9.1.4 9.1.5 9.1.6 9.2.0 9.2.0-beta.1 9.2.0-rc.1 9.2.1 9.2.2 9.2.3 9.2.4 9.2.5 9.3.0 9.3.0-beta.1 9.3.0-rc.1 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.3.6 9.4.0 9.4.0-beta.1 9.4.0-beta.2 9.4.0-rc.1 9.4.0-rc.2 9.4.0-rc.3 9.4.0-rc.4 9.4.1 9.4.2 9.4.3 9.4.4 9.4.5 9.5.0 9.5.0-beta.1 9.5.0-beta.2 9.5.0-rc.1 9.5.1 9.5.2 9.5.3 9.5.4 9.6.0 9.6.0-beta.1 9.6.0-beta.2 9.6.0-rc.1 9.6.1 9.6.2 9.6.3 9.6.4 9.7.0 9.7.0-beta.1 9.7.0-rc.1 9.7.1 9.7.2 9.7.3 9.8.0 9.8.0-beta.1 9.8.0-rc.1 9.8.1 9.8.2 9.8.3 9.8.4 9.8.5 9.8.6 9.8.7 9.9.0 9.9.0-beta.1 9.9.0-rc.1 9.9.1 9.9.2 9.9.3 9.9.4 9.9.5 9.9.6 9.9.7 3.7.3 7.1.2 3.8.0 7.2.0 3.8.0-beta.1 7.2.0-beta.1 3.8.0-rc.1 7.2.0-beta.2 3.8.0-rc.2 7.2.0-rc.1 3.8.1 7.2.0-rc.2 3.8.2 7.2.1 3.8.3 7.2.2 3.9.0 7.2.3 3.9.0-beta.1 7.2.4 3.9.0-beta.2 7.3.0 3.9.0-rc.1 7.3.0-beta.1 3.9.0-rc.2 7.3.0-beta.2 3.9.0-rc.3 7.3.0-rc.1 3.9.0-rc.4 7.3.0-rc.2 3.9.1 7.3.1 3.9.2 7.4.0 3.9.3 7.4.0-beta.1 3.9.4 7.4.0-beta.2 3.9.5 7.4.0-rc.1 4.0.0 7.4.0-rc.2 4.0.0-beta.1 7.4.1 4.0.0-rc.1 7.4.2 4.0.0-rc.2 7.5.0 4.0.1 7.5.0-beta.1 4.0.2 7.5.0-beta.2 4.0.3 7.5.0-rc.1 4.0.4 7.5.1 4.1.0 7.5.2 4.1.0-beta.1 7.6.0 4.1.0-beta.2 7.6.0-beta.1 4.1.0-rc.1 7.6.0-beta.2 4.1.0-rc.2 7.6.0-rc.1 4.1.1 7.6.0-rc.2 4.1.2 7.6.0-rc.3 4.1.3 7.6.1 4.1.4 7.6.2 4.2.0 7.7.0 4.2.0-RC.1 7.7.0-beta.1 4.2.0-RC.2 7.7.0-beta.2 4.2.0-beta.1 7.7.0-rc.1 4.2.1 7.7.1 4.2.2 7.7.2 4.2.3 7.7.3 4.2.4 7.8.0 4.2.5 4.3.0 4.3.0-beta.1 4.3.0-rc.1 4.3.0-rc.2 4.3.0-rc.3 4.3.1 4.3.2 4.3.3 4.3.4 4.3.5 4.3.6 4.4.0 4.4.0-beta.1 4.4.0-rc.1 4.4.1 4.4.2 4.4.3 4.4.4 4.5.0 4.5.0-beta.1 4.5.0-rc.1 4.5.0-rc.3 4.5.1 4.5.2 4.5.3 4.5.4 4.5.5 4.6.0 4.6.0-beta.1 4.6.0-rc.1 4.6.1 4.6.2 4.6.3 4.6.4 4.6.5 4.7.0 4.7.0-beta.1 4.7.0-beta.2 4.7.0-rc.1 4.7.1 4.7.1-beta.1 4.7.2 4.7.3 4.7.4 4.8.0 4.8.0-beta.1 4.8.0-rc.1 4.8.0-rc.2 4.8.1 4.8.2 4.8.3 4.9.0 4.9.0-beta.1 4.9.0-rc.1 4.9.0-rc.2 4.9.1 4.9.2 4.9.3 4.9.4 4.9.5 5.0.0 5.0.0-beta.1 5.0.0-beta.2 5.0.0-rc.1 5.0.0-rc.2 5.0.0-rc.3 5.0.1 5.0.2 5.0.3 5.1.0 5.1.0-beta.1 5.1.0-rc.1 trunk 5.1.1 10.0.0 5.1.2 10.0.0-rc.1 5.1.3 10.0.0-rc.2 5.2.0 10.0.1 5.2.0-beta.1 10.0.2 5.2.0-rc.1 10.0.3 5.2.0-rc.2 10.0.4 5.2.1 10.0.5 5.2.2 10.0.6 5.2.3 10.1.0 5.2.4 10.1.0-rc.1 5.2.5 10.1.0-rc.2 5.3.0 10.1.0-rc.3 5.3.0-beta.1 10.1.0-rc.4 5.3.0-rc.1 10.1.1 5.3.0-rc.2 10.1.2 5.3.1 10.1.3 5.3.2 10.1.4 5.3.3 10.2.0 5.4.0 10.2.0-beta.1 5.4.0-beta.1 10.2.0-beta.2 5.4.0-rc.1 10.2.0-rc.1 5.4.1 10.2.1 5.4.2 10.2.2 5.4.3 10.2.3 5.4.4 10.2.4 5.4.5 10.3.0 5.5.0 10.3.0-beta.1 5.5.0-beta.1 10.3.0-beta.2 5.5.0-rc.1 10.3.0-rc.1 5.5.0-rc.2 10.3.0-rc.2 5.5.1 10.3.1 5.5.2 10.3.2 5.5.3 10.3.3 5.5.4 10.3.4 5.5.5 10.3.5 5.6.0 10.3.6 5.6.0-beta.1 10.3.7 5.6.0-rc.1 10.3.8 5.6.0-rc.2 10.4.0 5.6.1 10.4.0-beta.1 5.6.2 10.4.0-beta.2 5.6.3 10.4.0-rc.1 5.7.0 10.4.1 5.7.0-beta.1 10.4.2 5.7.0-rc.1 10.4.3 5.7.1 10.4.4 5.7.2 10.5.0 5.7.3 10.5.0-beta.1 5.8.0 10.5.0-beta.2 5.8.0-beta.1 10.5.0-rc.1 5.8.0-beta.2 10.5.0-rc.2 5.8.0-rc.1 10.5.0-rc.3 5.8.1 10.5.1 5.8.2 10.5.2 5.9.0 10.5.3 5.9.0-beta.1 10.6.0 5.9.0-rc.1 10.6.0-beta.1 5.9.0-rc.2 10.6.0-beta.2 5.9.1 10.6.0-rc.1 5.9.2 10.6.1 6.0.0 10.6.2 6.0.0-beta.1 10.7.0 6.0.0-rc.1 10.7.0-beta.1 6.0.1 10.7.0-beta.2 6.0.2 10.7.0-rc.1 6.1.0 3.0.0 6.1.0-beta.1 3.0.1 6.1.0-rc.1 3.0.2 6.1.0-rc.2 3.0.3 6.1.1 3.0.4 6.1.2 3.0.5 6.1.3 3.0.6 6.2.0 3.0.7 6.2.0-beta.1 3.0.8 6.2.0-rc.1 3.0.9 6.2.0-rc.2 3.1.0 6.2.1 3.1.1 6.2.2 3.1.2 6.2.3 3.2.0 6.3.0 3.2.1 6.3.0-beta.1 3.2.2 6.3.0-rc.1 3.2.3 6.3.0-rc.2 3.2.4 6.3.1 3.2.5 6.3.2 3.2.6 6.4.0 3.3.0 6.4.0-beta.1 3.3.1 6.4.0-rc.1 3.3.2 6.4.1 3.3.2-rc.1 6.4.2 3.3.3 6.5.0 3.3.4 6.5.0-beta.1 3.3.5 6.5.0-rc.1 3.3.6 6.5.0-rc.2 3.4.0 6.5.1 3.4.0-beta.1 6.5.2 3.4.0-rc.2 6.6.0 3.4.1 6.6.0-beta.1 3.4.2 6.6.0-rc.1 3.4.3 6.6.0-rc.2 3.4.4 6.6.1 3.4.5 6.6.2 3.4.6 6.7.0 3.4.7 6.7.0-beta.1 3.4.8 6.7.0-beta.2 3.5.0 6.7.0-rc.1 3.5.0-beta.1 6.7.1 3.5.0-rc.1 6.8.0 3.5.0-rc.2 6.8.0-beta.1 3.5.1 6.8.0-beta.2 3.5.10 6.8.0-rc.1 3.5.2 6.8.1 3.5.3 6.8.2 3.5.4 6.8.3 3.5.5 6.9.0 3.5.6 6.9.0-beta.1 3.5.7 6.9.0-beta.2 3.5.8 6.9.0-rc.1 3.5.9 6.9.1 3.6.0 6.9.2 3.6.0-beta.1 6.9.3 3.6.0-rc.1 6.9.4 3.6.0-rc.2 6.9.5 3.6.0-rc.3 7.0.0 3.6.1 7.0.0-beta.1 3.6.2 7.0.0-beta.2 3.6.3 7.0.0-beta.3 3.6.4 7.0.0-rc.1 3.6.5 7.0.0-rc.2 3.6.6 7.0.1 3.6.7 7.0.2 3.7.0 7.1.0 3.7.0-beta.1 7.1.0-beta.1 3.7.0-rc.1 7.1.0-beta.2 3.7.0-rc.2 7.1.0-rc.1 3.7.1 7.1.0-rc.2 3.7.2 7.1.1
woocommerce / src / Internal / Admin / OrderMilestoneEasterEgg.php
woocommerce / src / Internal / Admin Last commit date
Agentic 4 months ago BlockTemplates 4 weeks ago EmailImprovements 1 year ago EmailPreview 4 weeks ago Emails 1 year ago ImportExport 1 year ago Logging 1 year ago Marketing 2 years ago Notes 4 weeks ago Onboarding 5 months ago Orders 4 weeks ago ProductForm 2 years ago ProductReviews 4 weeks ago RemoteFreeExtensions 4 weeks ago Schedulers 4 weeks ago Settings 5 days ago Suggestions 4 weeks ago WCPayPromotion 10 months ago ActivityPanels.php 3 years ago Analytics.php 4 weeks ago CategoryLookup.php 4 years ago Coupons.php 1 year ago CouponsMovedTrait.php 5 months ago CustomerEffortScoreTracks.php 7 months ago Events.php 2 months ago FeaturePlugin.php 4 months ago Homescreen.php 1 month ago Loader.php 4 weeks ago Marketing.php 1 year ago Marketplace.php 5 months ago MobileAppBanner.php 4 years ago OrderMilestoneEasterEgg.php 4 weeks ago RemoteInboxNotifications.php 3 years ago Settings.php 5 days ago ShippingLabelBanner.php 1 year ago ShippingLabelBannerDisplayRules.php 1 year ago SiteHealth.php 3 years ago Survey.php 4 years ago SystemStatusReport.php 1 year ago Translations.php 1 year ago WCAdminAssets.php 5 days ago WCAdminSharedSettings.php 1 year ago WCAdminUser.php 9 months ago WcPayWelcomePage.php 1 year ago
OrderMilestoneEasterEgg.php
451 lines
1 <?php
2
3 declare( strict_types = 1 );
4
5 namespace Automattic\WooCommerce\Internal\Admin;
6
7 use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
8 use Automattic\WooCommerce\Utilities\OrderUtil;
9
10 /**
11 * Displays a full-screen animated piñata overlay when a merchant opens a
12 * milestone order (1st, 100th, or 1000th real order) in the admin.
13 *
14 * Fires for any real paid order: status is processing or completed and a
15 * transaction ID is present.
16 *
17 * @since 10.9.0
18 */
19 class OrderMilestoneEasterEgg {
20
21 /**
22 * Option key used to cache computed milestone order IDs.
23 */
24 private const MILESTONE_CACHE_OPTION = '_wc_order_milestone_egg_order_ids';
25
26 /**
27 * Option key used to track whether all milestone order IDs have been found.
28 */
29 private const MILESTONES_COMPLETE_OPTION = '_wc_order_milestone_egg_milestones_complete';
30
31 /**
32 * Maximum number of qualifying orders needed to resolve all milestones.
33 */
34 private const MAX_QUALIFYING_ORDERS = 1000;
35
36 /**
37 * Milestone positions mapped to milestone message keys.
38 */
39 private const MILESTONE_POSITIONS = array(
40 0 => 'first',
41 99 => 'hundred',
42 999 => 'thousand',
43 );
44
45 /**
46 * Sets up the hooks.
47 *
48 * @internal
49 *
50 * @since 10.9.0
51 */
52 final public function init(): void {
53 add_action( 'admin_enqueue_scripts', array( $this, 'handle_admin_enqueue_scripts' ) );
54 add_action( 'wp_ajax_wc_egg_dismiss', array( $this, 'handle_ajax_dismiss' ) );
55 add_action( 'wp_ajax_wc_egg_opt_out', array( $this, 'handle_ajax_opt_out' ) );
56 add_action( 'woocommerce_new_order', array( $this, 'clear_milestone_cache' ), 10, 0 );
57 add_action( 'woocommerce_update_order', array( $this, 'clear_milestone_cache' ), 10, 0 );
58 add_action( 'woocommerce_delete_order', array( $this, 'clear_milestone_cache' ), 10, 0 );
59 add_action( 'woocommerce_trash_order', array( $this, 'clear_milestone_cache' ), 10, 0 );
60 }
61
62 /**
63 * Clears cached milestone order IDs until all milestones are complete.
64 *
65 * Once the 1st, 100th, and 1000th qualifying order IDs have been found,
66 * later orders cannot create additional milestone overlays, so keep the cache
67 * stable and avoid recomputing it after routine order changes.
68 *
69 * @internal
70 */
71 public function clear_milestone_cache(): void {
72 if ( wc_string_to_bool( get_option( self::MILESTONES_COMPLETE_OPTION, 'no' ) ) ) {
73 return;
74 }
75
76 delete_option( self::MILESTONE_CACHE_OPTION );
77 }
78
79 /**
80 * Opts the current user out of all future milestone overlays.
81 *
82 * @internal
83 */
84 public function handle_ajax_opt_out(): void {
85 check_ajax_referer( 'wc_egg_dismiss', 'nonce' );
86 update_user_meta( get_current_user_id(), '_wc_egg_opted_out', '1' );
87 wp_die();
88 }
89
90 /**
91 * Marks a milestone order as dismissed for the current user.
92 *
93 * @internal
94 */
95 public function handle_ajax_dismiss(): void {
96 check_ajax_referer( 'wc_egg_dismiss', 'nonce' );
97 $order_id = isset( $_POST['order_id'] ) ? absint( wp_unslash( $_POST['order_id'] ) ) : 0;
98 if ( $order_id > 0 ) {
99 update_user_meta( get_current_user_id(), '_wc_egg_seen_' . $order_id, '1' );
100 }
101 wp_die();
102 }
103
104 /**
105 * Enqueues the milestone overlay script when the current order is a qualifying milestone.
106 *
107 * @internal
108 */
109 public function handle_admin_enqueue_scripts(): void {
110 /**
111 * Filters whether the order milestone easter egg feature is enabled.
112 *
113 * Return false to disable the feature entirely — no order queries or assets will be loaded.
114 *
115 * @param bool $enabled Whether the feature is enabled. Default true.
116 *
117 * @since 10.9.0
118 */
119 if ( ! apply_filters( 'wc_order_milestone_egg_enabled', true ) ) {
120 return;
121 }
122
123 if ( ! function_exists( 'wc_get_order' ) ) {
124 return;
125 }
126
127 // phpcs:disable WordPress.Security.NonceVerification.Recommended
128 $woo_egg_key = isset( $_GET['woo_egg'] ) ? sanitize_text_field( wp_unslash( $_GET['woo_egg'] ) ) : '';
129 $page_param = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : '';
130 $action_param = isset( $_GET['action'] ) ? sanitize_text_field( wp_unslash( $_GET['action'] ) ) : '';
131 $id_param = isset( $_GET['id'] ) ? absint( wp_unslash( $_GET['id'] ) ) : 0;
132 // phpcs:enable WordPress.Security.NonceVerification.Recommended
133
134 // Preview: ?woo_egg=first|hundred|thousand lets admins preview any milestone without real orders.
135 // Only available when WP_DEBUG is enabled to prevent accidental triggering in production.
136 $is_debug_preview = ( defined( 'WP_DEBUG' ) && WP_DEBUG ) && current_user_can( 'manage_options' ) && '' !== $woo_egg_key;
137
138 // Respect the user's opt-out preference (debug preview always shows).
139 if ( ! $is_debug_preview && get_user_meta( get_current_user_id(), '_wc_egg_opted_out', true ) ) {
140 return;
141 }
142
143 // Only run milestone logic on the HPOS order edit page to avoid overhead on every admin page.
144 $is_order_edit_page = 'wc-orders' === $page_param && 'edit' === $action_param;
145
146 if ( ! $is_debug_preview && ! $is_order_edit_page ) {
147 return;
148 }
149
150 // For real order pages: check cheaply whether the current order qualifies
151 // before running the milestone lookup. The lookup relies on HPOS columns.
152 if ( ! $is_debug_preview ) {
153 if (
154 ! OrderUtil::custom_orders_table_usage_is_enabled()
155 || $id_param <= 0
156 || ! $this->is_qualifying_order( $id_param )
157 ) {
158 return;
159 }
160 }
161
162 $milestone_map = $is_debug_preview ? array() : $this->get_milestone_map();
163
164 if ( ! $is_debug_preview && empty( $milestone_map ) ) {
165 return;
166 }
167
168 // Remove milestones the current user has already seen.
169 if ( ! $is_debug_preview ) {
170 $user_id = get_current_user_id();
171 foreach ( array_keys( $milestone_map ) as $order_id ) {
172 if ( get_user_meta( $user_id, '_wc_egg_seen_' . $order_id, true ) ) {
173 unset( $milestone_map[ $order_id ] );
174 }
175 }
176 if ( empty( $milestone_map ) ) {
177 return;
178 }
179
180 // Only show the overlay when the current order is itself the milestone.
181 if ( ! isset( $milestone_map[ $id_param ] ) ) {
182 return;
183 }
184 $milestone_map = array( $id_param => $milestone_map[ $id_param ] );
185 }
186
187 // Only load the SVG variants needed for the matched milestones.
188 $all_msgs = array();
189 if ( $is_debug_preview ) {
190 $all_msgs = $this->get_milestone_messages();
191 $preview_variant = $all_msgs[ $woo_egg_key ]['variant'] ?? null;
192 $needed_variants = $preview_variant ? array( $preview_variant ) : array_keys( $this->get_variant_map() );
193 } else {
194 $needed_variants = array_unique(
195 array_filter( array_column( array_values( $milestone_map ), 'variant' ) )
196 );
197 }
198
199 $svg_data = $this->get_svg_data( $needed_variants );
200 $labels = $this->get_ui_labels();
201
202 WCAdminAssets::register_script( 'wp-admin-scripts', 'order-milestone-easter-egg', true );
203
204 $localize_data = array(
205 'milestones' => $milestone_map,
206 'svgData' => $svg_data,
207 'labels' => $labels,
208 'dismiss' => array(
209 'url' => admin_url( 'admin-ajax.php' ),
210 'nonce' => wp_create_nonce( 'wc_egg_dismiss' ),
211 ),
212 );
213
214 if ( $is_debug_preview ) {
215 $localize_data['allMilestones'] = $all_msgs;
216 }
217
218 wp_localize_script( 'wc-admin-order-milestone-easter-egg', 'wcOrderMilestoneEgg', $localize_data );
219 }
220
221 /**
222 * Returns true if the given order qualifies for milestone consideration: status is
223 * processing or completed, and a transaction ID is present.
224 *
225 * Used as a cheap pre-filter before running the full milestone count query.
226 *
227 * @param int $order_id The order ID to check.
228 * @return bool
229 */
230 public function is_qualifying_order( int $order_id ): bool {
231 $order = wc_get_order( $order_id );
232 if ( ! $order instanceof \WC_Order ) {
233 return false;
234 }
235 return '' !== $order->get_transaction_id()
236 && in_array( $order->get_status(), array( 'processing', 'completed' ), true );
237 }
238
239 /**
240 * Returns a map of milestone order IDs to their milestone data.
241 *
242 * Uses cached milestone order IDs when available; otherwise computes and caches
243 * them by scanning qualifying orders in chronological order.
244 *
245 * @return array<int, array<string, string>>
246 */
247 private function get_milestone_map(): array {
248 $milestone_order_ids = $this->get_cached_milestone_order_ids();
249 if ( null === $milestone_order_ids ) {
250 $milestone_order_ids = $this->compute_milestone_order_ids();
251 update_option( self::MILESTONE_CACHE_OPTION, $milestone_order_ids, false );
252 $this->update_milestones_complete_option( $milestone_order_ids );
253 }
254
255 $messages = $this->get_milestone_messages();
256 $milestone_map = array();
257
258 foreach ( $milestone_order_ids as $key => $order_id ) {
259 if ( isset( $messages[ $key ] ) ) {
260 $milestone_map[ $order_id ] = $messages[ $key ];
261 }
262 }
263
264 /**
265 * Filters the map of milestone order IDs to their milestone data.
266 *
267 * @param array<int, array<string, string>> $milestone_map Map of order ID to milestone data.
268 *
269 * @since 10.9.0
270 */
271 return apply_filters( 'wc_order_milestone_egg_map', $milestone_map );
272 }
273
274 /**
275 * Returns cached milestone order IDs, or null when the cache is missing.
276 *
277 * @return array<string, int>|null
278 */
279 private function get_cached_milestone_order_ids(): ?array {
280 $cached = get_option( self::MILESTONE_CACHE_OPTION, null );
281 if ( ! is_array( $cached ) ) {
282 return null;
283 }
284
285 $milestone_order_ids = array();
286 foreach ( self::MILESTONE_POSITIONS as $key ) {
287 if ( isset( $cached[ $key ] ) ) {
288 $order_id = absint( $cached[ $key ] );
289 if ( $order_id > 0 ) {
290 $milestone_order_ids[ $key ] = $order_id;
291 }
292 }
293 }
294
295 return $milestone_order_ids;
296 }
297
298 /**
299 * Updates the complete option when all milestone IDs have been found.
300 *
301 * @param array<string, int> $milestone_order_ids Milestone order IDs keyed by milestone name.
302 */
303 private function update_milestones_complete_option( array $milestone_order_ids ): void {
304 if ( count( $milestone_order_ids ) === count( self::MILESTONE_POSITIONS ) ) {
305 update_option( self::MILESTONES_COMPLETE_OPTION, 'yes', false );
306 return;
307 }
308
309 delete_option( self::MILESTONES_COMPLETE_OPTION );
310 }
311
312 /**
313 * Computes milestone order IDs from HPOS without hydrating order objects.
314 *
315 * @return array<string, int>
316 */
317 private function compute_milestone_order_ids(): array {
318 global $wpdb;
319
320 if ( ! OrderUtil::custom_orders_table_usage_is_enabled() ) {
321 return array();
322 }
323
324 $qualifying_order_ids = array_map(
325 'absint',
326 $wpdb->get_col(
327 $wpdb->prepare(
328 'SELECT id
329 FROM %i
330 WHERE type = %s
331 AND status IN ( %s, %s )
332 AND transaction_id IS NOT NULL
333 AND transaction_id <> %s
334 ORDER BY date_created_gmt ASC, id ASC
335 LIMIT %d',
336 OrdersTableDataStore::get_orders_table_name(),
337 'shop_order',
338 'wc-processing',
339 'wc-completed',
340 '',
341 self::MAX_QUALIFYING_ORDERS
342 )
343 )
344 );
345
346 $milestone_order_ids = array();
347 foreach ( self::MILESTONE_POSITIONS as $pos => $key ) {
348 if ( isset( $qualifying_order_ids[ $pos ] ) ) {
349 $milestone_order_ids[ $key ] = (int) $qualifying_order_ids[ $pos ];
350 }
351 }
352
353 return $milestone_order_ids;
354 }
355
356 /**
357 * Returns milestone copy and variant configuration keyed by milestone name.
358 *
359 * @return array<string, array<string, string>>
360 */
361 private function get_milestone_messages(): array {
362 return array(
363 'first' => array(
364 'title' => __( 'Cha-ching! Order number one', 'woocommerce' ),
365 'subtitle' => __( "That's a big deal. Smash the llama. You've earned it.", 'woocommerce' ),
366 'variant' => 'llama',
367 'boomText' => __( 'One down', 'woocommerce' ),
368 ),
369 'hundred' => array(
370 'title' => __( 'Triple digits looks good on you', 'woocommerce' ),
371 'subtitle' => __( "A hundred orders means you're juggling a lot. Take a moment to celebrate", 'woocommerce' ),
372 'variant' => 'octo',
373 'boomText' => __( 'Hands full', 'woocommerce' ),
374 ),
375 'thousand' => array(
376 'title' => __( 'ONE. THOUSAND. ORDERS', 'woocommerce' ),
377 'subtitle' => __( 'Seriously. A thousand orders. This called for a bigger piñata', 'woocommerce' ),
378 'variant' => 'whale',
379 'boomText' => __( 'Off the charts', 'woocommerce' ),
380 ),
381 );
382 }
383
384 /**
385 * Returns translated UI labels for the overlay script.
386 *
387 * @return array<string, string>
388 */
389 private function get_ui_labels(): array {
390 return array(
391 'cta' => __( "Let's go!", 'woocommerce' ),
392 'closeLabel' => __( 'Close', 'woocommerce' ),
393 'closeTitle' => __( 'Close (Esc)', 'woocommerce' ),
394 'optOut' => __( "Don't show again", 'woocommerce' ),
395 );
396 }
397
398 /**
399 * Returns the map of variant keys to their SVG filenames.
400 *
401 * @return array<string, string>
402 */
403 private function get_variant_map(): array {
404 return array(
405 'llama' => 'woo-pinata-llama2.svg',
406 'octo' => 'woo-octo.svg',
407 'whale' => 'woo-whale.svg',
408 );
409 }
410
411 /**
412 * Loads and returns SVG assets as inline strings.
413 *
414 * Only the variant SVGs listed in $variants are loaded; shared assets
415 * (confetti, stick, sprinkle) are always included.
416 *
417 * @param string[] $variants Variant keys to load (e.g. ['llama', 'octo']).
418 * @return array<string, string>
419 */
420 private function get_svg_data( array $variants = array() ): array {
421 $svg_dir = WC_ABSPATH . 'assets/images/pinata/';
422
423 if ( empty( $variants ) ) {
424 $variants = array_keys( $this->get_variant_map() );
425 }
426
427 $svg_data = array();
428 $variant_map = $this->get_variant_map();
429
430 // phpcs:disable WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
431 foreach ( $variants as $key ) {
432 if ( isset( $variant_map[ $key ] ) ) {
433 $svg_data[ $key ] = (string) file_get_contents( $svg_dir . $variant_map[ $key ] );
434 }
435 }
436
437 $svg_data['confetti'] = (string) file_get_contents( $svg_dir . 'confetti.svg' );
438 $svg_data['stick'] = (string) file_get_contents( $svg_dir . 'stick.svg' );
439 $sprinkle_svg = (string) file_get_contents( $svg_dir . 'sprinkle.svg' );
440 // phpcs:enable WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
441
442 $sprinkle_svg = preg_replace( '/<defs>.*?<\/defs>/s', '', $sprinkle_svg ) ?? '';
443 $sprinkle_svg = preg_replace( '/\s*clip-path="[^"]*"/', '', $sprinkle_svg ) ?? '';
444 $sprinkle_svg = preg_replace( '/<rect[^>]+fill="white"[^>]*\/?>/', '', $sprinkle_svg ) ?? '';
445
446 $svg_data['sprinkle'] = $sprinkle_svg;
447
448 return $svg_data;
449 }
450 }
451