14 || $minutes > 59 ) { return 'UTC'; } // POSIX Etc/GMT names invert the sign intentionally // (PHP/JS standard): Etc/GMT+5 ≡ UTC-5. $posix_sign = ( '-' === $sign ) ? '+' : '-'; $offset = sprintf( 'Etc/GMT%s%d', $posix_sign, $hours ); if ( 0 !== $minutes ) { // Half-hour and 45-minute zones: pick the closest canonical Etc/GMT. // PHP/JS only support whole-hour in `Etc/GMT*`, so fall back to UTC // for non-standard offsets rather than round (the BookingSession // still records the original offset separately — see docs/09 §6). return 'UTC'; } return $offset; } // `UTC+0`, `UTC-5` form. if ( preg_match( '/^UTC([+\-])(\d{1,2})$/i', $tz, $m ) === 1 ) { $sign = strtoupper( $m[1] ); $hours = (int) $m[2]; if ( $hours > 14 ) { return 'UTC'; } $posix_sign = ( '-' === $sign ) ? '+' : '-'; return sprintf( 'Etc/GMT%s%d', $posix_sign, $hours ); } return 'UTC'; } /** * Hook registration. */ public static function register(): void { add_shortcode( self::SHORTCODE, [ self::class, 'render' ] ); add_action( 'wp_enqueue_scripts', [ self::class, 'maybe_enqueue' ], 20 ); add_action( 'wp_footer', [ self::class, 'footer_fallback_enqueue' ], 5 ); } /** * Whether the current request should load widget assets. */ public static function should_enqueue(): bool { if ( ! CommercialRuntime::is_operational() ) { return false; } /** * Filter whether widget assets should load on this request. * * @var bool $forced */ $forced = apply_filters( 'bookline/frontend/should_enqueue', false ); if ( $forced ) { return true; } if ( ! is_singular() ) { return false; } $post = get_post(); if ( ! $post instanceof \WP_Post ) { return false; } return has_shortcode( $post->post_content, self::SHORTCODE ); } /** * Enqueue widget script when the shortcode is on the page. */ public static function maybe_enqueue(): void { if ( ! self::should_enqueue() ) { return; } self::enqueue_assets(); } /** * Late fallback for builder-injected shortcodes. */ public static function footer_fallback_enqueue(): void { if ( wp_script_is( 'bookline-widget', 'enqueued' ) ) { return; } if ( ! self::should_enqueue() ) { return; } self::enqueue_assets(); } /** * Register and enqueue built widget assets. */ private static function enqueue_assets(): void { $asset_file = BOOKLINE_PATH . 'assets/dist/widget.asset.php'; $script = BOOKLINE_URL . 'assets/dist/widget.js'; $deps = []; $version = BOOKLINE_VERSION; if ( is_readable( $asset_file ) ) { /** * Built asset metadata from @wordpress/scripts. * * @var array{dependencies?: string[], version?: string} $asset */ $asset = require $asset_file; $deps = $asset['dependencies'] ?? []; $version = $asset['version'] ?? BOOKLINE_VERSION; } wp_enqueue_style( 'bookline-widget-tokens', BOOKLINE_URL . 'assets/tokens.css', [], (string) filemtime( BOOKLINE_PATH . 'assets/tokens.css' ) ); wp_enqueue_style( 'bookline-widget-fonts', BOOKLINE_URL . 'assets/fonts.css', [], (string) filemtime( BOOKLINE_PATH . 'assets/fonts.css' ) ); $style = BOOKLINE_URL . 'assets/dist/widget.css'; wp_enqueue_style( 'bookline-widget', $style, [ 'bookline-widget-tokens', 'bookline-widget-fonts' ], $version ); wp_register_script( 'bookline-widget', $script, $deps, $version, true ); wp_enqueue_script( 'bookline-widget' ); } /** * Render widget mount point. * * @param array|string $atts Shortcode attributes. */ public static function render( $atts = [] ): string { if ( ! CommercialRuntime::is_operational() ) { return '
' . esc_html__( 'Booking is unavailable.', 'buildler' ) . '
'; } $atts = shortcode_atts( [ 'form' => '1', 'service' => '', 'agent' => '', 'location' => '', 'category' => '', 'mode' => 'inline', ], is_array( $atts ) ? $atts : [], self::SHORTCODE ); $form_id = max( 1, (int) $atts['form'] ); $theme = self::theme_for_form( $form_id ); $payload = [ 'restUrl' => esc_url_raw( rest_url( 'bookline/v1' ) ), 'nonce' => wp_create_nonce( 'wp_rest' ), 'formId' => $form_id, 'timezone' => self::normalise_timezone( wp_timezone_string() ), 'locale' => str_replace( '_', '-', get_locale() ), 'theme' => $theme, ]; $listed = self::named_entitlements(); if ( is_array( $listed ) ) { $payload['entitlements'] = $listed; } $paypal = self::paypal_widget_config(); if ( is_array( $paypal ) ) { $payload = array_merge( $payload, $paypal ); } $config = wp_json_encode( $payload ); if ( ! is_string( $config ) ) { $config = '{}'; } $classes = 'bookline-widget bookline-widget-root bookline-surface'; if ( 'inline' !== $atts['mode'] ) { $classes .= ' bookline-widget--' . sanitize_html_class( (string) $atts['mode'] ); } $extra = ''; foreach ( BookingFormThemeCss::html_attributes( $theme ) as $name => $value ) { $extra .= sprintf( ' %s="%s"', esc_attr( $name ), esc_attr( $value ) ); } return sprintf( '
', esc_attr( $classes ), esc_attr( wp_unique_id( 'bl' ) ), esc_attr( $config ), $form_id, $extra ); } /** * Load the six theme knobs for a published form, or the product defaults. * * @param int $form_id Form id from the shortcode. * @return array */ private static function theme_for_form( int $form_id ): array { $theme = BookingForm::default_theme(); try { $forms = Plugin::container()->get( BookingFormRepository::class ); if ( ! $forms instanceof BookingFormRepository ) { return $theme; } $form = $forms->find_by_id( $form_id ); if ( $form instanceof BookingForm ) { return $form->theme(); } } catch ( \Throwable ) { return $theme; } return $theme; } /** * Commercial plan keys, or null when this zip is unrestricted. * * @return list|null */ private static function named_entitlements(): ?array { try { $gate = Plugin::container()->get( NamedFeatureGate::class ); if ( $gate instanceof NamedFeatureGate ) { return $gate->listed(); } } catch ( \Throwable ) { return null; } return null; } /** * Public PayPal JS config when the commercial gateway is registered and configured. * * @return array{paypalClientId: string, paypalSandbox: bool}|null */ private static function paypal_widget_config(): ?array { try { $registry = Plugin::container()->get( PaymentGatewayRegistry::class ); if ( ! $registry instanceof PaymentGatewayRegistry || ! $registry->has( 'paypal' ) ) { return null; } $gateway = $registry->get( 'paypal' ); if ( ! $gateway->is_configured() ) { return null; } $cfg = $gateway->client_config(); $id = is_string( $cfg['client_id'] ?? null ) ? trim( (string) $cfg['client_id'] ) : ''; if ( '' === $id ) { return null; } return [ 'paypalClientId' => $id, 'paypalSandbox' => (bool) ( $cfg['sandbox'] ?? true ), ]; } catch ( \Throwable ) { return null; } } }