self::sync_webhook(null, $value), 10, 2); add_filter('plugin_action_links_'.plugin_basename(__FILE__), [__CLASS__, 'action_links']); add_action('before_woocommerce_init', static function () { if (class_exists(FeaturesUtil::class)) { FeaturesUtil::declare_compatibility('custom_order_tables', __FILE__, true); } }); } /** @return array{host:string,widget_token:string,bar_tokens:string,webhook_url:string,webhook_secret:string} */ public static function settings(): array { $saved = get_option(self::OPTION, []); return wp_parse_args(is_array($saved) ? $saved : [], [ 'host' => self::DEFAULT_HOST, 'widget_token' => '', 'bar_tokens' => '', 'webhook_url' => '', 'webhook_secret' => '', ]); } // ---------------------------------------------------------------- storefront /** * Enqueue the widget and one loader per bar. Each loader reads its token * from a data-token attribute, added through script_loader_tag below. */ public static function enqueue_scripts(): void { $s = self::settings(); $host = untrailingslashit(esc_url_raw($s['host'] ?: self::DEFAULT_HOST)); $args = ['strategy' => 'async', 'in_footer' => true]; if ($s['widget_token'] !== '') { wp_enqueue_script('provedly-widget', $host . '/w/widget.js', [], null, $args); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion -- versioned by the service self::$tokens['provedly-widget'] = $s['widget_token']; } foreach (preg_split('/[\s,]+/', (string) $s['bar_tokens']) ?: [] as $token) { $token = trim($token); if ($token === '') { continue; } $handle = 'provedly-bar-' . sanitize_key($token); wp_enqueue_script($handle, $host . '/w/cta.js', [], null, $args); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion -- versioned by the service self::$tokens[$handle] = $token; } } /** @var array handle => token */ private static array $tokens = []; public static function script_tag(string $tag, string $handle): string { if (! isset(self::$tokens[$handle])) { return $tag; } return str_replace(' src=', ' data-token="' . esc_attr(self::$tokens[$handle]) . '" defer src=', $tag); } /** * Copy the widget's anonymous visitor id onto the order so Provedly can * attribute the purchase to a notification the buyer saw (30-day window). * The id is random, set by the widget, and contains no personal data. */ public static function remember_visitor($order): void { if (! is_object($order) || ! method_exists($order, 'update_meta_data')) { return; } $vid = ''; foreach (self::VISITOR_COOKIES as $cookie) { if (isset($_COOKIE[$cookie])) { $vid = sanitize_text_field(wp_unslash($_COOKIE[$cookie])); break; } } if ($vid !== '' && strlen($vid) <= 64) { $order->update_meta_data(self::ORDER_META, $vid); } } // ---------------------------------------------------------------- webhook /** * Create (or update) the WooCommerce webhook that delivers orders to * Provedly, signed with the secret from the Provedly dashboard. */ public static function sync_webhook($old, $new): void { if (! is_array($new) || empty($new['webhook_url']) || ! class_exists('WC_Webhook')) { return; } $url = esc_url_raw($new['webhook_url']); $secret = (string) ($new['webhook_secret'] ?? ''); // Reuse the webhook this plugin created before (by id, so a changed URL // updates it instead of leaving a stale one), else adopt one with the same URL. $existing = self::own_webhook() ?: self::find_webhook($url); $webhook = $existing ?: new WC_Webhook; $webhook->set_name('Provedly – order updates'); $webhook->set_topic('order.updated'); $webhook->set_delivery_url($url); $webhook->set_secret($secret); $webhook->set_status('active'); $webhook->set_api_version('wp_api_v3'); // WooCommerce builds the payload through the REST API as this user; with no // user (WP-CLI, cron) the payload would be a permission error, so fall back // to an administrator. if (! $existing || ! $webhook->get_user_id()) { $webhook->set_user_id(self::webhook_user_id()); } $webhook->save(); update_option(self::WEBHOOK_OPTION, $webhook->get_id(), false); } private static function webhook_user_id(): int { $current = get_current_user_id(); if ($current > 0) { return $current; } $admins = get_users(['role' => 'administrator', 'number' => 1, 'orderby' => 'ID', 'order' => 'ASC', 'fields' => 'ID']); return $admins ? (int) $admins[0] : 0; } private static function own_webhook() { $id = (int) get_option(self::WEBHOOK_OPTION, 0); if ($id <= 0 || ! function_exists('wc_get_webhook')) { return null; } $hook = wc_get_webhook($id); return $hook && $hook->get_id() ? $hook : null; } private static function find_webhook(string $url) { if (! class_exists('WC_Data_Store')) { return null; } try { $store = WC_Data_Store::load('webhook'); foreach ($store->search_webhooks(['limit' => 50]) as $id) { $hook = wc_get_webhook($id); if ($hook && $hook->get_delivery_url() === $url) { return $hook; } } } catch (Throwable $e) { return null; } return null; } // ---------------------------------------------------------------- admin public static function menu(): void { // WordPress recolours a base64 SVG menu icon to match the admin colour scheme. $icon = (string) file_get_contents(__DIR__.'/assets/menu-icon.svg'); add_menu_page('Provedly', 'Provedly', 'manage_options', 'provedly', [__CLASS__, 'render_page'], 'data:image/svg+xml;base64,'.base64_encode($icon), 58); } /** Until 1.0.2 the page lived under Settings; keep old bookmarks working. */ public static function redirect_old_settings_url(): void { global $pagenow; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only redirect, no state change $page = isset($_GET['page']) ? sanitize_key(wp_unslash($_GET['page'])) : ''; if ($pagenow === 'options-general.php' && $page === 'provedly') { wp_safe_redirect(admin_url('admin.php?page=provedly')); exit; } } public static function action_links(array $links): array { array_unshift($links, ''.esc_html__('Settings', 'provedly').''); return $links; } public static function register_settings(): void { register_setting('provedly', self::OPTION, [ 'type' => 'array', 'sanitize_callback' => [__CLASS__, 'sanitize'], 'default' => [], ]); } public static function sanitize($input): array { $input = is_array($input) ? $input : []; $host = isset($input['host']) ? esc_url_raw(trim((string) $input['host'])) : self::DEFAULT_HOST; return [ 'host' => $host !== '' ? $host : self::DEFAULT_HOST, 'widget_token' => preg_replace('/[^a-z0-9_]/', '', strtolower((string) ($input['widget_token'] ?? ''))), 'bar_tokens' => implode("\n", array_filter(array_map( static fn ($t) => preg_replace('/[^a-z0-9_]/', '', strtolower(trim($t))), preg_split('/[\s,]+/', (string) ($input['bar_tokens'] ?? '')) ?: [] ))), 'webhook_url' => isset($input['webhook_url']) ? esc_url_raw(trim((string) $input['webhook_url'])) : '', 'webhook_secret' => sanitize_text_field((string) ($input['webhook_secret'] ?? '')), ]; } public static function render_page(): void { if (! current_user_can('manage_options')) { return; } $s = self::settings(); $woo = class_exists('WooCommerce'); $hook = $s['webhook_url'] !== '' && $woo ? (self::own_webhook() ?: self::find_webhook($s['webhook_url'])) : null; ?>

Provedly