* only submitting its OWN fields, switching to another tab and saving * would submit an empty value for every field on the tab you're NOT * looking at, silently wiping it back to blank/default. Keeping * everything in one form/one settings group sidesteps that entirely. */ class SEATT_Settings { /** * Sample prices in the Display previews are written in rupiah-sized * numbers (150,000). Scale them so a USD or EUR store sees $35, not * $150,000.00, and a yen or rupee store sees something in between. */ private static function preview_price( $amount ) { $code = strtoupper( (string) get_option( 'seatt_currency_code', 'IDR' ) ); $large = array( 'IDR', 'VND', 'LAK', 'KHR', 'MMK', 'KRW', 'UZS', 'IRR', 'COP', 'CLP', 'PYG', 'UGX', 'TZS' ); $mid = array( 'JPY', 'INR', 'PHP', 'THB', 'RUB', 'TWD', 'PKR', 'BDT', 'LKR', 'NPR', 'NGN', 'KES', 'ARS', 'EGP', 'HUF', 'ISK' ); if ( in_array( $code, $large, true ) ) { return $amount; } return in_array( $code, $mid, true ) ? $amount / 100 : round( $amount / 4285.7142857 ); } public function __construct() { add_action( 'admin_menu', array( $this, 'register_menu' ) ); add_action( 'admin_init', array( $this, 'register_settings' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue' ) ); } public function enqueue( $hook ) { $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- which screen is open if ( 'seatt-settings' !== $page ) { return; } wp_enqueue_style( 'seatt-shared', SEATT_PLUGIN_URL . 'admin/css/esm-shared.css', array(), seatt_asset_ver( 'admin/css/esm-shared.css' ) ); } public function register_menu() { add_submenu_page( 'seatt-main', __( 'Settings', 'seathive' ), __( 'Settings', 'seathive' ), 'manage_options', 'seatt-settings', array( $this, 'render_page' ) ); } public function register_settings() { // Store Setting register_setting( 'seatt_settings', 'seatt_currency_code', array( 'sanitize_callback' => function ( $v ) { $v = strtoupper( sanitize_text_field( (string) $v ) ); return array_key_exists( $v, seatt_currency_list() ) ? $v : 'IDR'; } ) ); register_setting( 'seatt_settings', 'seatt_currency_symbol', array( 'sanitize_callback' => 'sanitize_text_field' ) ); register_setting( 'seatt_settings', 'seatt_show_tax_in_cart', array( 'type' => 'boolean', 'sanitize_callback' => 'rest_sanitize_boolean' ) ); register_setting( 'seatt_settings', 'seatt_tax_rate', array( 'type' => 'number', 'sanitize_callback' => function ( $v ) { return max( 0, (float) str_replace( ',', '.', (string) $v ) ); } ) ); register_setting( 'seatt_settings', 'seatt_fee_type', array( 'sanitize_callback' => function ( $v ) { return in_array( $v, array( 'none', 'percent', 'per_ticket' ), true ) ? $v : 'none'; } ) ); register_setting( 'seatt_settings', 'seatt_fee_value', array( 'type' => 'number', 'sanitize_callback' => function ( $v ) { return max( 0, (float) $v ); } ) ); register_setting( 'seatt_settings', 'seatt_fee_label', array( 'sanitize_callback' => 'sanitize_text_field' ) ); register_setting( 'seatt_settings', 'seatt_reminder_enabled', array( 'sanitize_callback' => function ( $v ) { return $v ? 1 : 0; } ) ); register_setting( 'seatt_settings', 'seatt_reminder_days', array( 'sanitize_callback' => function ( $v ) { return max( 0, min( 14, (int) $v ) ); } ) ); register_setting( 'seatt_settings', 'seatt_reminder_template', array( 'sanitize_callback' => 'sanitize_textarea_field' ) ); // Payment Setting (unchanged from before — just relocated into this tabbed page) register_setting( 'seatt_settings', 'seatt_display_event', array( 'sanitize_callback' => array( 'SEATT_Display_Extra', 'sanitize_event' ) ) ); register_setting( 'seatt_settings', 'seatt_display_tickets', array( 'sanitize_callback' => array( 'SEATT_Display_Extra', 'sanitize_tickets' ) ) ); register_setting( 'seatt_settings', 'seatt_display_checkout', array( 'sanitize_callback' => array( 'SEATT_Display_Extra', 'sanitize_checkout' ) ) ); register_setting( 'seatt_settings', 'seatt_display_receipt', array( 'sanitize_callback' => array( 'SEATT_Display_Extra', 'sanitize_receipt' ) ) ); register_setting( 'seatt_settings', 'seatt_display_mytickets', array( 'sanitize_callback' => array( 'SEATT_Display_Extra', 'sanitize_mytickets' ) ) ); register_setting( 'seatt_settings', SEATT_Door_Theme::OPT, array( 'sanitize_callback' => array( 'SEATT_Door_Theme', 'sanitize' ) ) ); register_setting( 'seatt_settings', 'seatt_payment_gateway', array( 'sanitize_callback' => function ( $v ) { return in_array( $v, array( 'manual', 'xendit', 'stripe', 'doku', 'paypal', 'mollie' ), true ) ? $v : 'manual'; } ) ); register_setting( 'seatt_settings', 'seatt_mollie_api_key', array( 'sanitize_callback' => array( 'SEATT_Security', 'sanitize_secret' ) ) ); register_setting( 'seatt_settings', 'seatt_mollie_currency', array( 'sanitize_callback' => function ( $v ) { $v = strtoupper( sanitize_text_field( $v ) ); return ( '' === $v || SEATT_Payment::mollie_supports( $v ) ) ? $v : ''; } ) ); register_setting( 'seatt_settings', 'seatt_mollie_rate', array( 'sanitize_callback' => function ( $v ) { return max( 0, (float) str_replace( ',', '.', (string) $v ) ); } ) ); register_setting( 'seatt_settings', 'seatt_max_seats_per_order', array( 'type' => 'integer', 'sanitize_callback' => function ( $v ) { return max( 1, min( 100, (int) $v ) ); } ) ); register_setting( 'seatt_settings', 'seatt_cash_expiry_hours', array( 'type' => 'integer', 'sanitize_callback' => function ( $v ) { return max( 0, min( 720, (int) $v ) ); } ) ); // 'manual' | 'xendit' | 'stripe' | 'paypal' register_setting( 'seatt_settings', 'seatt_xendit_secret_key', array( 'sanitize_callback' => array( 'SEATT_Security', 'sanitize_secret' ) ) ); register_setting( 'seatt_settings', 'seatt_xendit_callback_token', array( 'sanitize_callback' => array( 'SEATT_Security', 'sanitize_secret' ) ) ); // "Verification Token" from Xendit dashboard register_setting( 'seatt_settings', 'seatt_stripe_secret_key', array( 'sanitize_callback' => array( 'SEATT_Security', 'sanitize_secret' ) ) ); register_setting( 'seatt_settings', 'seatt_stripe_webhook_secret', array( 'sanitize_callback' => array( 'SEATT_Security', 'sanitize_secret' ) ) ); // "Signing secret" from the Stripe Dashboard webhook endpoint register_setting( 'seatt_settings', 'seatt_doku_client_id', array( 'sanitize_callback' => array( 'SEATT_Security', 'sanitize_secret' ) ) ); register_setting( 'seatt_settings', 'seatt_doku_secret_key', array( 'sanitize_callback' => array( 'SEATT_Security', 'sanitize_secret' ) ) ); register_setting( 'seatt_settings', 'seatt_doku_production', array( 'type' => 'boolean', 'sanitize_callback' => 'rest_sanitize_boolean' ) ); register_setting( 'seatt_settings', 'seatt_paypal_client_id', array( 'sanitize_callback' => array( 'SEATT_Security', 'sanitize_secret' ) ) ); register_setting( 'seatt_settings', 'seatt_paypal_client_secret', array( 'sanitize_callback' => array( 'SEATT_Security', 'sanitize_secret' ) ) ); register_setting( 'seatt_settings', 'seatt_paypal_production', array( 'type' => 'boolean', 'sanitize_callback' => 'rest_sanitize_boolean' ) ); register_setting( 'seatt_settings', 'seatt_paypal_currency', array( 'sanitize_callback' => function ( $v ) { $v = strtoupper( sanitize_text_field( $v ) ); return ( '' === $v || SEATT_Payment::paypal_supports( $v ) ) ? $v : ''; } ) ); register_setting( 'seatt_settings', 'seatt_paypal_rate', array( 'sanitize_callback' => function ( $v ) { return max( 0, (float) str_replace( ',', '.', (string) $v ) ); } ) ); // Email // SMTP. The password is registered like any other option, but // wp-config's SEATT_SMTP_PASSWORD takes precedence at read time — // see SEATT_SMTP::password() for why that is the better place. register_setting( 'seatt_settings', 'seatt_smtp_enabled', array( 'type' => 'boolean', 'sanitize_callback' => 'rest_sanitize_boolean' ) ); register_setting( 'seatt_settings', 'seatt_smtp_preset', array( 'sanitize_callback' => 'sanitize_key' ) ); register_setting( 'seatt_settings', 'seatt_smtp_host', array( 'sanitize_callback' => 'sanitize_text_field' ) ); register_setting( 'seatt_settings', 'seatt_smtp_port', array( 'type' => 'integer', 'sanitize_callback' => 'absint' ) ); register_setting( 'seatt_settings', 'seatt_smtp_encryption', array( 'sanitize_callback' => function ( $v ) { return in_array( $v, array( 'tls', 'ssl', 'none' ), true ) ? $v : 'tls'; } ) ); register_setting( 'seatt_settings', 'seatt_smtp_auth', array( 'type' => 'boolean', 'sanitize_callback' => 'rest_sanitize_boolean' ) ); register_setting( 'seatt_settings', 'seatt_smtp_username', array( 'sanitize_callback' => 'sanitize_text_field' ) ); register_setting( 'seatt_settings', 'seatt_smtp_password', array( 'sanitize_callback' => array( 'SEATT_Security', 'sanitize_secret' ) ) ); register_setting( 'seatt_settings', 'seatt_smtp_from_email', array( 'sanitize_callback' => 'sanitize_email' ) ); register_setting( 'seatt_settings', 'seatt_smtp_from_name', array( 'sanitize_callback' => 'sanitize_text_field' ) ); register_setting( 'seatt_settings', 'seatt_send_receipt_email', array( 'type' => 'boolean', 'sanitize_callback' => 'rest_sanitize_boolean' ) ); register_setting( 'seatt_settings', 'seatt_receipt_email_text', array( 'sanitize_callback' => 'wp_kses_post' ) ); // Client Order Placed Email register_setting( 'seatt_settings', 'seatt_client_order_placed_enabled', array( 'type' => 'boolean', 'sanitize_callback' => 'rest_sanitize_boolean' ) ); register_setting( 'seatt_settings', 'seatt_client_order_placed_subject', array( 'sanitize_callback' => 'sanitize_text_field' ) ); register_setting( 'seatt_settings', 'seatt_client_order_placed_content', array( 'sanitize_callback' => 'wp_kses_post' ) ); // Display — one option holding every event's theme plus a // 'default'. Arrives from the tab as JSON in a single hidden // field, so the whole map saves in the same round trip as every // other tab (see this class's header note about one form). register_setting( 'seatt_settings', 'seatt_display_themes', array( 'type' => 'array', 'sanitize_callback' => array( $this, 'sanitize_display_themes' ), 'default' => array(), ) ); } /** * Rebuilds the theme map from scratch on every save rather than * merging into what's already stored — that way an event deleted in * the tab actually disappears instead of lingering forever. */ public function sanitize_display_themes( $input ) { // The tab posts discrete seatt_display_themes[key][prop] fields, so // this arrives as an array. Anything else means a truncated or // tampered request, and blanking every event's theme would be a // destructive way to react to that — keep what's stored instead. if ( ! is_array( $input ) ) { return seatt_get_display_themes(); } $clean = array(); foreach ( $input as $key => $theme ) { if ( ! is_array( $theme ) ) { continue; } // Keys are either the literal 'default' or a real event ID. if ( 'default' === $key ) { $clean['default'] = seatt_sanitize_display_theme( $theme ); continue; } $event_id = intval( $key ); if ( $event_id > 0 && 'seatt_event' === get_post_type( $event_id ) ) { $clean[ $event_id ] = seatt_sanitize_display_theme( $theme ); } } return $clean; } public function render_page() { if ( ! current_user_can( 'manage_options' ) ) return; $gateway = get_option( 'seatt_payment_gateway', 'manual' ); $webhook_url = rest_url( 'seatt/v1/payment-webhook' ); $currency = get_option( 'seatt_currency_code', 'IDR' ); $currencies = seatt_currency_list(); $active_tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'general'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- which tab to show ?>
' . ( esc_js( __( 'Sending…', 'seathive' ) ) ) . '\'; fetch(' . ( wp_json_encode( esc_url_raw( rest_url( 'seatt/v1/smtp-test' ) ) ) ) . ', { method: \'POST\', headers: { \'Content-Type\': \'application/json\', \'X-WP-Nonce\': ' . ( wp_json_encode( wp_create_nonce( 'wp_rest' ) ) ) . ' }, credentials: \'same-origin\', body: JSON.stringify({ to: to }) }).then(function (res) { return res.json().then(function (data) { return { ok: res.ok, data: data }; }); }).then(function (r) { testBtn.disabled = false; if (r.ok) { out.innerHTML = \'\' + ' . ( wp_json_encode( __( 'Sent. Check the inbox — and the spam folder, since that is where an unauthenticated sender usually lands.', 'seathive' ) ) ) . ' + \'
\' + ' . ( wp_json_encode( __( 'Not sent.', 'seathive' ) ) ) . ' + \' \' + String(msg).replace(/[&<>"]/g, function (c) { return { \'&\': \'&\', \'<\': \'<\', \'>\': \'>\', \'"\': \'"\' }[c]; }) + \'
\' + ' . ( wp_json_encode( __( 'Could not reach the site to run the test.', 'seathive' ) ) ) . ' + \'