<?php
/**
* The main front-end controller. This controller will directly, or by delegation, subscribe to
* front-end related hooks.
*
* @since 5.16.0
*
* @package TEC\Controller;
*/
namespace TEC\Tickets\Seating;
use TEC\Common\Contracts\Provider\Controller as Controller_Contract;
use TEC\Common\lucatume\DI52\Container;
use TEC\Common\Asset;
use Tribe__Tickets__Commerce__Currency;
use TEC\Tickets\Seating\Admin\Ajax;
use TEC\Tickets\Seating\Frontend\Session;
use TEC\Tickets\Seating\Frontend\Timer;
use TEC\Tickets\Seating\Service\Service;
use Tribe__Template as Base_Template;
use Tribe__Tickets__Main as ET;
use Tribe__Tickets__Tickets as Tickets;
use WP_Error;
use Tribe__Tickets__Ticket_Object as Ticket_Object;
use Tribe__Main as Common;
use TEC\Tickets\Commerce\Checkout;
use TEC\Tickets\Seating\Orders\Cart;
use Tribe__Tickets__Tickets_Handler as Tickets_Handler;
/**
* Class Controller.
*
* @since 5.16.0
*
* @package TEC\Controller;
*/
class Frontend extends Controller_Contract {
/**
* The ID of the modal used to display the seat selection modal.
*
* @since 5.16.0
*
* @var string
*/
public const MODAL_ID = 'tec-tickets-seating-seat-selection-modal';
/**
* The action that will be fired when this Controller registers.
*
* @since 5.16.0
*
* @var string
*/
public static string $registration_action = 'tec_tickets_seating_frontend_registered';
/**
* A reference to the template object.
*
* @since 5.16.0
*
* @var Template
*/
private Template $template;
/**
* A reference to the service object.
*
* @since 5.16.0
*
* @var Service
*/
private Service $service;
/**
* Controller constructor.
*
* @since 5.16.0
*
* @param Container $container A reference to the container object.
* @param Template $template A reference to the template object.
* @param Service $service A reference to the service object.
*/
public function __construct( Container $container, Template $template, Service $service ) {
parent::__construct( $container );
$this->template = $template;
$this->service = $service;
}
/**
* Unregisters the Controller by unsubscribing from WordPress hooks.
*
* @since 5.16.0
*
* @return void
*/
public function unregister(): void {
remove_filter( 'tribe_template_pre_html:tickets/v2/tickets', [ $this, 'print_tickets_block' ] );
remove_filter( 'tribe_tickets_block_ticket_html_attributes', [ $this, 'add_seat_selected_labels_per_ticket_attribute' ] );
}
/**
* Replace the Tickets' block with the one starting the seat selection flow.
*
* @since 5.16.0
*
* @param string $html The initial HTML.
* @param string $file Complete path to include the PHP File.
* @param array $name Template name.
* @param Base_Template $template Current instance of the Tribe__Template.
* @param array<string,mixed> $context The context data passed to the template.
*
* @return string|null The template HTML, or `null` to let the default template process it.
*/
public function print_tickets_block( $html, $file, $name, $template, $context ): ?string {
$data = $template->get_values();
$post_id = $data['post_id'] ?? null;
if ( ! ( $post_id && tec_tickets_seating_enabled( $post_id ) ) ) {
return $html;
}
$provider = Tickets::get_event_ticket_provider_object( $post_id );
if ( ! $provider ) {
return $html;
}
// Bail if there are no tickets on sale.
if ( empty( $data['has_tickets_on_sale'] ) ) {
return $html;
}
$service_status = $this->service->get_status();
if ( ! $service_status->is_ok() ) {
$html = $this->template->template( 'tickets-block-error', [], false );
/**
* Filters the contents of the Tickets block when there is a problem with the service.
*
* @since 5.16.0
*
* @param string $html The HTML of the Tickets block.
* @param Template $template A reference to the template object.
*/
$html = apply_filters( 'tec_tickets_seating_tickets_block_html', $html, $template );
return $html;
}
$prices = [];
foreach ( tribe_tickets()->where( 'event', $post_id )->get_ids( true ) as $ticket_id ) {
$ticket = $provider->get_ticket( $post_id, $ticket_id );
if ( ! $ticket ) {
continue;
}
$prices[ $ticket->price ] = true;
}
if ( ! count( $prices ) ) {
// Why are we here at all?
return $html;
}
$prices = array_keys( $prices );
$inventory = $this->get_events_ticket_capacity_for_seating( $post_id );
/** @var Tribe__Tickets__Commerce__Currency $currency */
$currency = tribe( 'tickets.commerce.currency' );
$cost_range = count( $prices ) === 1 ? $currency->get_formatted_currency_with_symbol( $prices[0], $post_id, $provider, false ) :
$currency->get_formatted_currency_with_symbol( min( $prices ), $post_id, $provider, false )
. ' - '
. $currency->get_formatted_currency_with_symbol( max( $prices ), $post_id, $provider, false );
$timeout = $this->container->get( Timer::class )->get_timeout( $post_id );
$html = $this->template->template(
'tickets-block',
[
'cost_range' => $cost_range,
'inventory' => $inventory,
'modal_content' => 0 === $inventory ? '' : $this->get_seat_selection_modal_content( $post_id, $timeout ),
'timeout' => $timeout,
],
false
);
/**
* Filters the contents of the Tickets block.
*
* @since 5.16.0
*
* @param string $html The HTML of the Tickets block.
* @param Template $template A reference to the template object.
*/
$html = apply_filters( 'tec_tickets_seating_tickets_block_html', $html, $template );
return $html;
}
/**
* Adjusts the event's ticket capacity to consider seating.
*
* @since 5.16.0
*
* @param int $post_id The event ID.
*
* @return int The number of available ASC tickets for the post.
*/
public function get_events_ticket_capacity_for_seating( int $post_id ): int {
if ( ! tec_tickets_seating_enabled( $post_id ) ) {
return 0;
}
$provider = Tickets::get_event_ticket_provider_object( $post_id );
if ( ! $provider ) {
return 0;
}
$available = [];
foreach ( tribe_tickets()->where( 'event', $post_id )->get_ids( true ) as $ticket_id ) {
$ticket = $provider->get_ticket( $post_id, $ticket_id );
if ( ! $ticket ) {
continue;
}
$seat_type = get_post_meta( $ticket->ID, Meta::META_KEY_SEAT_TYPE, true );
if ( empty( $available[ $seat_type ] ) ) {
// The array's keys are the seating types. In order for us to calculate the stock per type and NOT per ticket.
$available[ $seat_type ] = $ticket->stock();
continue;
}
$available[ $seat_type ] = $available[ $seat_type ] < $ticket->stock() ? $available[ $seat_type ] : $ticket->stock();
}
if ( empty( $available ) ) {
return 0;
}
return array_sum( $available );
}
/**
* Returns the HTML content of the seat selection modal.
*
* @since 5.16.0
*
* @param int $post_id The post ID of the post to purchase tickets for.
* @param int $timeout The timeout in seconds.
*
* @return string The HTML content of the seat selection modal.
*/
private function get_seat_selection_modal_content( int $post_id, int $timeout ): string {
/*
* While the user might have 15 minutes to purchase tickets, that timer will not start on page load,
* but when the user starts the interaction withe the seat selection modal.
* For this reason the token request is made with a TTL of 4 times the timeout.
*/
$ephemeral_token_ttl = $timeout * 4;
$ephemeral_token = $this->service->get_ephemeral_token( $ephemeral_token_ttl, 'visitor' );
$token = is_string( $ephemeral_token ) ? $ephemeral_token : '';
$iframe_url = $this->service->get_seat_selection_url( $token, $post_id, $ephemeral_token_ttl );
/** @var \Tribe\Dialog\View $dialog_view */
$dialog_view = tribe( 'dialog.view' );
$provider = Tickets::get_event_ticket_provider_object( $post_id );
/** @var Tribe__Tickets__Commerce__Currency $currency */
$currency = tribe( 'tickets.commerce.currency' );
$content = $this->template->template(
'iframe-view',
[
'iframe_url' => $iframe_url,
'token' => $token,
'error' => $ephemeral_token instanceof WP_Error ? $ephemeral_token->get_error_message() : '',
'initial_total_text' => _x( '0 Tickets', 'Seat selection modal initial total string', 'event-tickets' ),
'initial_total_price' => $currency->get_formatted_currency_with_symbol( 0, $post_id, $provider, false ),
'post_id' => $post_id,
],
false
);
$args = [
'button_text' => esc_html_x( 'Find Seats', 'Find seats button text', 'event-tickets' ),
'button_classes' => [ 'tribe-common-c-btn', 'tribe-common-c-btn--small' ],
'append_target' => '.tec-tickets-seating__information',
'content_wrapper_classes' => 'tribe-dialog__wrapper tec-tickets-seating__modal',
'overlay_click_closes' => false,
];
return $dialog_view->render_modal( $content, $args, self::MODAL_ID, false );
}
/**
* Registers the controller by subscribing to front-end hooks and binding implementations.
*
* @since 5.16.0
*
* @return void
*/
protected function do_register(): void {
add_filter( 'tribe_template_pre_html:tickets/v2/tickets', [ $this, 'print_tickets_block' ], 10, 5 );
add_filter( 'tribe_tickets_block_ticket_html_attributes', [ $this, 'add_seat_selected_labels_per_ticket_attribute' ], 10, 2 );
// Register the front-end JS.
Asset::add(
'tec-tickets-seating-frontend',
'frontend/ticketsBlock.js',
ET::VERSION
)
->add_to_group_path( 'tec-seating' )
->set_condition( [ $this, 'should_enqueue_assets' ] )
->set_dependencies(
'tribe-dialog-js',
'tec-tickets-seating-service-bundle',
'tec-tickets-seating-currency',
'wp-hooks',
'tec-tickets-seating-session'
)
->add_localize_script(
'tec.tickets.seating.frontend.ticketsBlockData',
fn() => $this->get_ticket_block_data( get_the_ID() )
)
->enqueue_on( 'wp_enqueue_scripts' )
->add_to_group( 'tec-tickets-seating-frontend' )
->add_to_group( 'tec-tickets-seating' )
->register();
// Register the front-end CSS.
Asset::add(
'tec-tickets-seating-frontend-style',
'frontend/style-ticketsBlock.css',
ET::VERSION
)
->add_to_group_path( 'tec-seating' )
->set_condition( [ $this, 'should_enqueue_assets' ] )
->enqueue_on( 'wp_enqueue_scripts' )
->add_to_group( 'tec-tickets-seating-frontend' )
->add_to_group( 'tec-tickets-seating' )
->register();
}
/**
* Checks if the current context is the Block Editor and the post type is ticket-enabled.
*
* @since 5.17.0
*
* @return bool Whether the assets should be enqueued or not.
*/
public function should_enqueue_assets() {
$ticketable_post_types = (array) tribe_get_option( 'ticket-enabled-post-types', [] );
if ( empty( $ticketable_post_types ) ) {
return false;
}
return ( tribe( Checkout::class )->is_current_page() && tribe( Cart::class )->cart_has_seating_tickets() ) || ( is_singular( $ticketable_post_types ) && tec_tickets_seating_enabled( Common::post_id_helper() ) );
}
/**
* Adds the seat selected labels to the ticket block.
*
* @since 5.16.0
* @since 5.18.1 Removed the $event_id parameter.
*
* @param array $attributes The attributes of the ticket block.
* @param Ticket_Object $ticket The ticket object.
*
* @return array The attributes of the ticket block.
*/
public function add_seat_selected_labels_per_ticket_attribute( array $attributes, Ticket_Object $ticket ): array {
$event_id = $ticket->get_event_id();
if ( ! $event_id ) {
return $attributes;
}
if ( ! tec_tickets_seating_enabled( $event_id ) ) {
return $attributes;
}
$reservations = tribe( Session::class )->get_post_ticket_reservations( $event_id, $ticket->ID );
if ( empty( $reservations ) || ! is_array( $reservations ) ) {
return $attributes;
}
$reservations = implode(
',',
$reservations ? array_values(
wp_list_pluck( $reservations, 'seat_label' )
) : []
);
$attributes['data-seat-labels'] = esc_attr( $reservations );
return $attributes;
}
/**
* Returns the data to be localized on the ticket block frontend.
*
* @since 5.16.0
*
* @param int $post_id The post ID.
*
* @return array{
* objectName: string,
* modalId: string,
* seatTypeMap: array<array{
* id: string,
* tickets: array<array{
* ticketId: string,
* name: string,
* price: string,
* description: string
* }>
* }>
* } The data to be localized on the ticket block frontend.
*/
public function get_ticket_block_data( $post_id ): array {
$service_ok = $this->service->get_status()->is_ok();
$data = [
'objectName' => 'dialog_obj_' . self::MODAL_ID,
'modalId' => self::MODAL_ID,
'seatTypeMap' => $service_ok ? $this->build_seat_type_map( $post_id ) : [],
'labels' => [
'oneTicket' => esc_html( _x( '1 Ticket', 'Seat selection modal total string', 'event-tickets' ) ),
'multipleTickets' => esc_html(
_x( '{count} Tickets', 'Seat selection modal total string', 'event-tickets' )
),
],
'providerClass' => esc_html( Tickets::get_event_ticket_provider( $post_id ) ),
'postId' => $post_id,
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'ajaxNonce' => wp_create_nonce( Ajax::NONCE_ACTION ),
'ACTION_POST_RESERVATIONS' => Ajax::ACTION_POST_RESERVATIONS,
'ACTION_CLEAR_RESERVATIONS' => Ajax::ACTION_CLEAR_RESERVATIONS,
'sessionTimeout' => tribe( Timer::class )->get_timeout( $post_id ),
];
/**
* Filters the data to be localized on the ticket block frontend.
*
* @since 5.20.1
*
* @param array<string,mixed> $data The data to be localized on the ticket block frontend.
* @param int $post_id The post ID.
*/
return apply_filters( 'tec_tickets_seating_frontend_ticket_block_data', $data, $post_id );
}
/**
* Builds the Seat Type map localized for the seat selection modal.
*
* @since 5.16.0
*
* @param int|null $post_id The current post ID.
*
* @return array{
* id: string,
* tickets: array<array{
* ticketId: string,
* name: string,
* price: string,
* description: string,
* maxLimit: int
* }>
* } The Seat Type map.
*/
public function build_seat_type_map( ?int $post_id = null ): array {
if ( ! $post_id ) {
return [];
}
$seat_type_map = [];
$provider = null;
foreach ( tribe_tickets()->where( 'event', $post_id )->get_ids( true ) as $ticket_id ) {
// The provider will be the same for all tickets in the loop, just init once.
/** @var \Tribe__Tickets__Tickets $provider */
$provider ??= tribe_tickets_get_ticket_provider( $ticket_id );
$ticket = $provider->get_ticket( $post_id, $ticket_id );
if ( ! $ticket instanceof Ticket_Object ) {
continue;
}
$seat_type = get_post_meta( $ticket_id, Meta::META_KEY_SEAT_TYPE, true );
if ( ! $seat_type ) {
continue;
}
if ( ! isset( $seat_type_map[ $seat_type ] ) ) {
$seat_type_map[ $seat_type ] = [
'id' => $seat_type,
'tickets' => [],
];
}
/** @var Tickets_Handler $tickets_handler */
$tickets_handler = tribe( 'tickets.handler' );
/** @var Tribe__Tickets__Commerce__Currency $currency */
$currency = tribe( 'tickets.commerce.currency' );
$seat_type_map[ $seat_type ]['tickets'][] = [
'ticketId' => $ticket_id,
'name' => $ticket->name,
'price' => $currency->get_formatted_currency_with_symbol( $ticket->price, $post_id, $provider, false ),
'priceValue' => $ticket->price,
'description' => $ticket->description,
'dateInRange' => $ticket->date_in_range(),
'maxLimit' => $tickets_handler->get_ticket_max_purchase( $ticket_id ),
];
}
return $seat_type_map;
}
}
|