` and nothing after it can set a header. The strings do * not come back translated either. */ public function register(): void { $dist = WCONVERT_DIR . self::DIST; /* * ==================================================================== * UNCONDITIONAL, AND A MISSING BUNDLE DOES NOT CHANGE THAT. * ==================================================================== * The obvious guard here is `is_file($dist) || return`, and it is * backwards. **An unregistered dynamic block renders nothing**, so * skipping registration on a checkout with no build does not degrade * the editor — it takes the Optin off every page that already carries * this block, on the front end, for visitors. Registering with a * script that 404s costs one broken block in the editor and leaves * every saved one rendering. * * Where a missing bundle is actually caught is the release: * `bin/verify-artifact-contract.sh` fails closed on it before a ZIP * exists, beside the loader, the inspector and the two admin chunks. * {@see BuiltAsset::version()} falls back to the plugin version when * the file is not there, so nothing here needs to ask twice. */ wp_register_script( self::HANDLE, WCONVERT_URL . self::DIST, self::EDITOR_DEPENDENCIES, BuiltAsset::version($dist), true ); wp_register_style( self::HANDLE, WCONVERT_URL . self::DIST_STYLE, [], BuiltAsset::version(WCONVERT_DIR . self::DIST_STYLE) ); wp_set_script_translations(self::HANDLE, 'wconvert'); // The metadata directory, plus the one thing metadata cannot carry. // Everything else about this block — its name, its category, its one // attribute — is declared in `block.json` and read by the editor // bundle from the same file, so there is no second spelling to drift. register_block_type(WCONVERT_DIR . self::METADATA, [ // All supported WordPress versions provide block API v3. 'api_version' => 3, 'render_callback' => [self::class, 'render'], ]); // Only where a block can be placed. This reads an option and runs a // query, and `init` fires on every request of every install — so the // list is built on the editor's own hook rather than beside the // registration above. add_action('enqueue_block_editor_assets', [$this, 'provideTheOptinList']); add_action('rest_api_init', [$this, 'registerRoute']); } /** * The picker's Refresh: a GET, answered fresh every time. * * `no-store` because the whole point of the read is that the list changed * since the page loaded; a cached answer is the stale one it replaces. */ public function registerRoute(): void { register_rest_route(Routes::NAMESPACE, self::ROUTE, [ 'methods' => 'GET', 'permission_callback' => [Routes::class, 'canPlaceCampaign'], 'callback' => fn (): \WP_REST_Response => new \WP_REST_Response($this->choices(), 200, ['Cache-Control' => 'no-store']), ]); } /** * The anchor for the Optin this block names. * * Static, and a pure function of its argument, for the reason * {@see LoaderEnqueue::withCartUrl()} is: it is the half of this class a * unit suite can hold to the shortcode's output, and everything around it * is WordPress calls that suite cannot make. * * @param array $attributes The block's saved attributes. */ public static function render(array $attributes): string { return InlineAnchor::html((string) ($attributes['optinId'] ?? '')); } /** * Hand the editor the list its picker is built from. * * `'before'`, so the data is on `window` by the time the bundle's IIFE * runs. `wp_add_inline_script` writes a classic `