'Gatecraft', 'brand_tagline' => 'Live events worth leaving the house for', 'brand_color' => '#0b0b0c', 'brand_logo' => '', 'brand_logo_dark' => '', 'brand_favicon' => '', 'default_cover' => '', 'button_shape' => 'rounded', 'header_style' => 'light', 'hero_style' => 'next', 'event_list_view' => 'medium', 'ticket_layout' => 'side', 'event_design' => '', 'ui_style' => 'theme', 'hero_image' => '', 'font' => 'inter', // Site text and messages 'home_headline' => 'Live events worth leaving the house for', 'home_intro' => 'Book in a minute and get your QR ticket by email.', 'search_placeholder' => 'Search by event, artist or venue', 'empty_message' => 'No events right now. New dates are added often.', 'label_sold_out' => 'Sold out', 'label_low_stock' => 'Only {left} left', 'low_stock_threshold' => '10', 'label_upcoming' => 'On sale from {date}', 'btn_checkout' => 'Continue to payment', 'btn_checkout_approval' => 'Send request for approval', 'approval_notice' => 'These tickets need approval. Your seats are held while we review, and you only pay once approved.', 'msg_paid' => 'You are going. We have emailed your tickets with a calendar invite.', 'msg_awaiting_approval' => 'Request received. We will email you as soon as there is a decision.', 'msg_pending_payment' => 'Your tickets are reserved until {pay_deadline}. Complete payment to confirm them.', 'msg_closed' => 'This booking is closed and its tickets are no longer valid.', 'footer_text' => '', 'support_email' => '', 'support_phone' => '', 'social_instagram' => '', 'social_x' => '', 'social_linkedin' => '', 'social_facebook' => '', 'social_website' => '', 'footer_links' => '', 'meta_description' => '', // Email 'mail_from_name' => '', 'reply_to' => '', 'email_footer' => 'You received this email because you booked with us.', 'email_templates' => '{}', // Checkout and payments 'pay_card' => '1', 'pay_bank' => '1', 'bank_instructions' => "Please transfer the amount due to:\n[Bank name, branch, account type and number, account holder]\nQuote your booking reference so we can match the payment.", 'guest_checkout' => '1', 'phone_mode' => 'optional', 'require_terms' => '1', 'default_max_per_order' => '10', 'hold_minutes' => '20', 'pay_konbini' => '0', 'pay_paypay_stripe' => '0', 'konbini_days' => '3', 'offline_hold_hours' => '48', 'offline_methods' => '', 'approval_hold_days' => '7', 'tax_note' => 'Prices include consumption tax.', // Notifications 'alert_paid_app' => '1', 'alert_paid_email' => '0', 'alert_bank_app' => '1', 'alert_bank_email' => '1', 'alert_capacity_app' => '1', 'alert_capacity_email' => '1', 'alert_payment_app' => '1', 'alert_payment_email' => '1', 'alert_mailfail_app' => '1', 'alert_mailfail_email' => '0', 'capacity_threshold' => '90', 'alert_recipients' => 'staff', 'alert_emails' => '', 'expired_email' => '1', 'daily_summary_enabled' => '0', 'daily_summary_time' => '08:30', // Sign-in and security 'staff_password_login' => '1', 'session_idle_minutes' => '120', 'staff_session_hours' => '8', 'login_max_attempts' => '5', 'login_lock_minutes' => '15', 'code_tries' => '30', 'admin_ip_allowlist' => '', // Legal 'terms_text' => '', 'refund_text' => '', 'privacy_text' => '', 'checkout_agreement' => 'I agree to the terms of sale and the refund policy.', 'business_details' => '', // Regional 'timezone' => '', 'date_format' => 'en', 'time_format' => '24', // WordPress specific 'base_slug' => 'events', 'show_ticket_bar' => '1', 'staff_2fa' => '1', 'free_email_confirm' => '1', 'delete_data_on_uninstall' => '0', 'currency' => 'JPY', 'currency_decimals' => '0', 'mail_from_address' => '', // Internal (not shown in the form) 'daily_summary_last' => '', ]; const EMAIL_TEMPLATES = [ 'tickets' => ['label' => 'Booking confirmed, tickets attached', 'when' => 'When a booking is paid, or a free booking is confirmed.', 'qr' => true, 'subject' => 'Your tickets: {event_title}', 'heading' => 'You are going to {event_title}', 'body' => "Hi {buyer_name}, your booking {booking_reference} is confirmed.\nShow the QR code for each ticket at the entrance. Each code can be used once.", 'button' => 'View tickets online'], 'confirm_email' => ['label' => 'Free booking: confirm email', 'when' => 'When someone books free tickets, before the tickets are issued. The link in it confirms the booking.', 'subject' => 'Confirm your booking: {event_title}', 'heading' => 'One more step for your free tickets', 'body' => "Hi {buyer_name}, please confirm your email address to get your tickets for {event_title}.\nThe link works until {pay_deadline}. After that the places go back to other guests.\nIf you did not book, you can ignore this email.", 'button' => 'Confirm and get my tickets'], 'payment_pending' => ['label' => 'Offline payment: instructions', 'when' => 'When a buyer chooses an offline payment method, such as bank transfer, PayPay or cash.', 'subject' => 'Complete your payment: {event_title}', 'heading' => 'Your tickets are reserved', 'body' => "Hi {buyer_name}, thank you for your booking. We are holding your tickets until {pay_deadline}.\nYour payment details and the event information are below. We will email your QR tickets as soon as we confirm your payment.", 'button' => 'View your booking'], 'konbini_pending' => ['label' => 'Konbini: payment slip', 'when' => 'When a buyer chooses to pay at a convenience store.', 'subject' => 'Pay at the convenience store: {event_title}', 'heading' => 'Your tickets are reserved', 'body' => "Hi {buyer_name}, thank you for your booking {booking_reference}.\nPay {amount} at FamilyMart, Lawson, Ministop or Seicomart by {pay_deadline}, using the payment slip. If the slip is not paid by then, the booking expires and the seats go back on sale.\nWe will email your QR tickets as soon as the store confirms the payment.", 'button' => 'Open the payment slip'], 'cancelled' => ['label' => 'Booking cancelled', 'when' => 'When staff cancel a booking.', 'subject' => 'Booking cancelled: {event_title}', 'heading' => 'Your booking has been cancelled', 'body' => "Hi {buyer_name}, booking {booking_reference} has been cancelled and its tickets are no longer valid.\n{staff_note}", 'button' => 'View your booking'], 'refunded' => ['label' => 'Booking refunded', 'when' => 'When staff refund a booking.', 'subject' => 'Refund issued: {event_title}', 'heading' => 'Your booking has been refunded', 'body' => 'Hi {buyer_name}, we have refunded {amount} for booking {booking_reference}. Card refunds usually appear within 5 to 10 business days.', 'button' => ''], 'expired' => ['label' => 'Reservation expired', 'when' => 'When an unpaid hold runs out.', 'subject' => 'Reservation expired: {event_title}', 'heading' => 'Your reservation has expired', 'body' => 'Hi {buyer_name}, we did not receive payment for booking {booking_reference} in time, so the tickets were released.', 'button' => 'See the event'], 'event_cancelled' => ['label' => 'Event cancelled', 'when' => 'When an event is cancelled, to every confirmed buyer.', 'subject' => 'Cancelled: {event_title}', 'heading' => '{event_title} has been cancelled', 'body' => "Hi {buyer_name}, we are sorry to tell you that {event_title} on {event_date} will not go ahead.\n{staff_note}", 'button' => 'View your booking'], 'staff_invite' => ['label' => 'Staff: invitation', 'when' => 'When an admin invites someone who has no account yet.', 'subject' => 'Your access to {brand_name}', 'heading' => 'Set up your {brand_name} account', 'body' => "You have been given {role} access to {brand_name}.\nUse the button below to choose your password. The link works once and expires on {expires}.\nIf you did not expect this email, you can ignore it.", 'button' => 'Set up my account'], 'staff_alert' => ['label' => 'Staff: alert email', 'when' => 'Copies of staff alerts, sent to alert recipients.', 'subject' => '[{brand_name}] {alert_title}', 'heading' => '{alert_title}', 'body' => '{alert_body}', 'button' => 'Open in admin'], ]; const EMAIL_VARIABLES = ['buyer_name', 'event_title', 'event_date', 'venue', 'doors_time', 'booking_reference', 'amount', 'pay_deadline', 'staff_note', 'payment_method', 'payment_instructions', 'event_note', 'brand_name']; function settings_cache(bool $reset = false): array { static $cache = null; if ($reset) $cache = null; if ($cache === null) { $cache = []; try { foreach (q('SELECT k, v FROM {settings}')->fetchAll() as $r) $cache[$r['k']] = $r['v']; } catch (Throwable $t) { log_debug('settings: ' . $t->getMessage()); } } return $cache; } function setting(string $k): string { return settings_cache()[$k] ?? (setting_defaults()[$k] ?? ''); } function setting_on(string $k): bool { return setting($k) === '1'; } function setting_int(string $k, int $min, int $max): int { return max($min, min($max, (int)setting($k))); } function setting_save(array $values): void { foreach ($values as $k => $v) { if (!array_key_exists($k, setting_defaults())) continue; q('INSERT INTO {settings} (k, v) VALUES (?, ?) ON DUPLICATE KEY UPDATE v = VALUES(v)', [$k, (string)$v]); } settings_cache(true); } /** One email template, with saved edits merged over the defaults. */ function email_template(string $id): array { $def = email_templates()[$id] ?? null; if (!$def) throw new RuntimeException(esc_html('Unknown email template ' . $id)); $saved = json_decode(setting('email_templates'), true); $mine = is_array($saved[$id] ?? null) ? $saved[$id] : []; $out = $def + ['enabled' => true]; foreach (['subject', 'heading', 'body', 'button'] as $f) if (isset($mine[$f]) && is_string($mine[$f])) $out[$f] = $mine[$f]; if (isset($mine['enabled'])) $out['enabled'] = (bool)$mine['enabled']; return $out; } /** Replace {variables}. Values are plain text; escaping happens when the email is rendered. */ function fill_vars(string $text, array $vars): string { if (array_key_exists('booking_reference', $vars)) $vars['order_reference'] = $vars['booking_reference']; // the name used in earlier templates return preg_replace_callback('/\{([a-z_]+)\}/', fn($m) => array_key_exists($m[1], $vars) ? (string)$vars[$m[1]] : $m[0], $text); } /** * "Match my theme": the colours, fonts and button style the active block theme already defines, so ticket pages look * like the rest of the site. Returns CSS custom properties as name => value, or [] when Gatecraft styling is chosen. * A theme value is kept only if it is a hex colour, rgb()/hsl(), a theme variable or a font stack; the caller still * passes every value through css_value() when it prints them (see css_vars()). */ function theme_style_vars(): array { $mode = setting('ui_style'); if ($mode === 'brand') return []; $vars = ['--font' => 'inherit', '--bg' => 'transparent', '--ink' => 'currentColor']; if (wp_is_block_theme() && function_exists('wp_get_global_styles')) { $o = ['transforms' => ['resolve_variables' => true]]; $color = static function (array $path) use ($o): string { $v = wp_get_global_styles($path, [], $o); if (!is_string($v)) return ''; $v = trim($v); if (sanitize_hex_color($v)) return strtolower($v); return preg_match('/^(rgba?\([0-9 ,.\/%]+\)|hsla?\([0-9 ,.\/%a-z]+\)|var\(--wp--[a-z0-9-]+(--[a-z0-9-]+)*\))$/i', $v) ? $v : ''; }; $font = static function (array $path) use ($o): string { $f = wp_get_global_styles($path, [], $o); return is_string($f) && preg_match('/^(var\(--wp--[a-z0-9-]+(--[a-z0-9-]+)*\)|[a-z0-9 ,"\'-]{2,200})$/i', trim($f)) ? trim($f) : ''; }; $btnBg = $color(['elements', 'button', 'color', 'background']); $btnTx = $color(['elements', 'button', 'color', 'text']); $text = $color(['color', 'text']); $page = $color(['color', 'background']); $body = $font(['typography', 'fontFamily']); $head = $font(['elements', 'heading', 'typography', 'fontFamily']); if ($mode === 'theme' && $btnBg !== '' && $btnTx !== '') { $vars['--brand'] = $btnBg; $vars['--on-brand'] = $btnTx; $vars['--brand-dark'] = 'color-mix(in srgb,' . $btnBg . ' 82%,#000)'; } if ($text !== '') $vars['--ink'] = $text; if ($page !== '') $vars['--surface'] = $page; if ($head !== '') $vars['--font-head'] = $head; if ($body !== '') $vars['--font'] = $body; } // Without a theme text colour, --ink follows the surrounding text. Filled chips, pills and dark buttons need a real colour. if ($vars['--ink'] === 'currentColor') $vars['--ink-solid'] = '#1f2328'; return $vars + [ '--warm' => 'color-mix(in srgb,var(--brand) 55%,var(--surface,#fff))', '--ink-2' => 'color-mix(in srgb,var(--ink) 88%,transparent)', '--muted' => 'color-mix(in srgb,var(--ink) 70%,transparent)', '--line' => 'color-mix(in srgb,var(--ink) 15%,transparent)', '--line-2' => 'color-mix(in srgb,var(--ink) 8%,transparent)', ]; } /** Extra class on the Gatecraft wrapper of public pages. */ function ui_class(): string { return setting('ui_style') === 'brand' ? '' : ' gc-theme'; } function brand_color(): string { $c = setting('brand_color'); return preg_match('/^#[0-9a-fA-F]{6}$/', $c) ? strtolower($c) : '#0b0b0c'; } function brand_shade(float $factor): string { $parts = array_map('hexdec', str_split(substr(brand_color(), 1), 2)); // Near-black colours (the Gatecraft default) cannot get darker, so their hover shade is lighter instead. if (max($parts) < 48) return vsprintf('#%02x%02x%02x', array_map(fn($c) => (int)round($c + (255 - $c) * (1 - $factor)), $parts)); $rgb = array_map(fn($c) => (int)round(max(0, min(255, $c * $factor))), $parts); return sprintf('#%02x%02x%02x', ...$rgb); } /** Readable text colour on the brand colour (WCAG relative luminance). */ function brand_on_color(): string { [$r, $g, $b] = array_map(function ($p) { $c = hexdec($p) / 255; return $c <= 0.03928 ? $c / 12.92 : (($c + 0.055) / 1.055) ** 2.4; }, str_split(substr(brand_color(), 1), 2)); return 0.2126 * $r + 0.7152 * $g + 0.0722 * $b > 0.4 ? '#0b0d12' : '#ffffff'; } function brand_initials(): string { $words = preg_split('/\s+/', trim(setting('brand_name'))) ?: []; $s = ''; foreach ($words as $w) $s .= mb_substr($w, 0, 1); return mb_strtoupper(mb_substr($s, 0, 2)) ?: 'EV'; } function card_enabled(): bool { return setting_on('pay_card') && cfg('stripe_secret', '') !== ''; } function staff_2fa_required(): bool { return setting_on('staff_2fa'); } /** * Offline payment methods (bank transfer, PayPay, cash...). The buyer follows the instructions outside the * system; staff confirm the payment, which issues the tickets. Stored as JSON in the offline_methods setting. */ function offline_methods_all(): array { $list = json_decode(setting('offline_methods'), true); if (!is_array($list)) { $list = [ ['id' => 'bank', 'name' => 'Bank transfer', 'on' => setting_on('pay_bank'), 'instructions' => setting('bank_instructions')], ['id' => 'paypay', 'name' => 'PayPay', 'on' => false, 'instructions' => "Send {amount} with PayPay to: [your PayPay ID]\nWrite your booking reference {booking_reference} in the message."], ['id' => 'cash', 'name' => 'Cash', 'on' => false, 'instructions' => "Pay {amount} in cash at: [place and times]\nShow your booking reference {booking_reference}."], ]; } $out = []; foreach ($list as $m) { if (!is_array($m) || !preg_match('/^[a-z0-9-]{1,40}$/', (string)($m['id'] ?? '')) || trim((string)($m['name'] ?? '')) === '') continue; $out[$m['id']] = ['id' => $m['id'], 'name' => mb_substr(trim((string)$m['name']), 0, 60), 'on' => !empty($m['on']), 'instructions' => mb_substr((string)($m['instructions'] ?? ''), 0, 2000)]; } return $out; } function offline_methods(): array { return array_filter(offline_methods_all(), fn($m) => $m['on']); } /** * Payment choices for one event: key "stripe" or "offline:", with name and instructions. * An event stores the methods it allows (payment_methods JSON); empty means every method that is on. */ function event_payment_options(array $ev): array { $allowed = json_decode((string)($ev['payment_methods'] ?? ''), true); $own = event_payment_details($ev); $build = function (?array $allowed) use ($own): array { $opts = []; if (card_enabled() && ($allowed === null || in_array('card', $allowed, true))) $opts['stripe'] = ['key' => 'stripe', 'name' => stripe_choice_label(), 'instructions' => null]; foreach (offline_methods() as $m) { if ($allowed === null || in_array($m['id'], $allowed, true)) $opts['offline:' . $m['id']] = ['key' => 'offline:' . $m['id'], 'name' => $m['name'], 'instructions' => $own[$m['id']] ?? $m['instructions']]; } return $opts; }; $opts = $build(is_array($allowed) ? $allowed : null); return $opts ?: $build(null); // if the event's choices were all switched off, fall back to whatever is on } /** Payment instructions this event uses instead of the defaults, per offline method id. */ function event_payment_details(array $ev): array { $d = json_decode((string)($ev['payment_details'] ?? ''), true); if (!is_array($d)) return []; return array_filter(array_map(fn($v) => is_string($v) ? mb_substr(trim($v), 0, 2000) : '', $d), fn($v, $k) => $v !== '' && preg_match('/^[a-z0-9-]{1,40}$/', (string)$k), ARRAY_FILTER_USE_BOTH); } function bank_enabled(): bool { return (bool)offline_methods(); } /** True when an add-on offers "Sign in with Google" on this site (Gatecraft Pro). */ function google_signin_on(): bool { return (bool)apply_filters('gatecraft_google_signin_on', false); }