'; return 'data:image/svg+xml;base64,' . base64_encode( $svg ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- WordPress's documented format for a menu icon. } /** * Adds a Settings link on the Plugins screen. * * @param array $links Existing links. * @return array */ public static function action_links( $links ) { array_unshift( $links, '' . esc_html__( 'Settings', 'walletpassbuilder' ) . '' ); return $links; } /** * The admin page's address. * * @param string $tab Tab. * @param array $args Extra query arguments. * @return string */ public static function url( $tab = '', $args = array() ) { $query = array( 'page' => self::SLUG ); if ( '' !== $tab ) { $query['tab'] = $tab; } return add_query_arg( $query + $args, admin_url( 'admin.php' ) ); } /** * Loads the admin page's styles and scripts. * * @param string $hook Current admin page. */ public static function assets( $hook ) { if ( 'toplevel_page_' . self::SLUG !== $hook ) { return; } wp_enqueue_style( 'walletpb-admin', WALLETPB_URL . 'assets/css/admin.css', array( 'walletpb-button' ), WALLETPB_VERSION ); wp_enqueue_media(); // QR Code Generator by Kazuhiko Arase (MIT), bundled unmodified, for the live preview. wp_enqueue_script( 'walletpb-qrcode', WALLETPB_URL . 'assets/js/vendor/qrcode.js', array(), '2.0.4', true ); wp_enqueue_script( 'walletpb-admin', WALLETPB_URL . 'assets/js/admin.js', array( 'jquery', 'walletpb-qrcode' ), WALLETPB_VERSION, true ); wp_localize_script( 'walletpb-admin', 'walletpbAdmin', array( 'chooseLogo' => __( 'Choose a logo', 'walletpassbuilder' ), 'useLogo' => __( 'Use this image', 'walletpassbuilder' ), 'confirmUpdate' => __( 'Update the pass? Everyone who already saved it gets the new design on their phone.', 'walletpassbuilder' ), 'confirmBooking' => __( 'Save the booking pass? Booking passes already on customers\' phones update to it.', 'walletpassbuilder' ), 'copied' => __( 'Copied', 'walletpassbuilder' ), 'matches' => self::variation_matches(), 'emptyValue' => __( 'empty', 'walletpassbuilder' ), 'mainImage' => __( 'Main image', 'walletpassbuilder' ), 'chooseAnImage' => __( 'Choose an image', 'walletpassbuilder' ), /* translators: %s: an example value, from the preview's sample booking. */ 'exampleValue' => __( 'For example: %s', 'walletpassbuilder' ), 'valueLabel' => __( 'Value', 'walletpassbuilder' ), /* translators: %d: how many screenshots a message can carry. */ 'tooManyShots' => __( 'Choose up to %d screenshots.', 'walletpassbuilder' ), /* translators: %s: the screenshot's file name. */ 'removeShot' => __( 'Remove %s', 'walletpassbuilder' ), 'tooLong' => __( 'Too long for this code', 'walletpassbuilder' ), 'change' => __( 'Change', 'walletpassbuilder' ), 'defaultLogo' => WalletPB_Template::default_logo_url(), 'templates' => self::template_fields(), 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'placesNonce' => wp_create_nonce( 'walletpb_places' ), 'chooseImage' => __( 'Choose an image', 'walletpassbuilder' ), 'useImage' => __( 'Use this image', 'walletpassbuilder' ), 'chooseLabel' => __( 'Choose image', 'walletpassbuilder' ), 'noPlaces' => __( 'No matches. Try a fuller address.', 'walletpassbuilder' ), 'maxPlaces' => WalletPB_Advanced::MAX_LOCATIONS, 'insert' => __( 'Insert a data field', 'walletpassbuilder' ), 'insertShort' => __( 'Data Field', 'walletpassbuilder' ), 'maxBack' => WalletPB_Template::MAX_BACK, ) ); } /* ---------- notices ---------- */ /** * Saves a notice to show after the redirect. * * @param string $type "success", "error" or "warning". * @param string $message Message. */ private static function notice( $type, $message ) { set_transient( 'walletpb_notice_' . get_current_user_id(), array( $type, $message ), 60 ); } /** * Shows and clears the saved notice. */ private static function render_notice() { $key = 'walletpb_notice_' . get_current_user_id(); $notice = get_transient( $key ); if ( ! is_array( $notice ) ) { return; } delete_transient( $key ); printf( '

%2$s

', esc_attr( $notice[0] ), esc_html( $notice[1] ) ); } /** * Stops a form handler unless the user may manage options. Each handler checks its nonce itself. */ private static function require_admin() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You do not have permission to do that.', 'walletpassbuilder' ), '', array( 'response' => 403 ) ); } } /* ---------- handlers ---------- */ /** * Saves the API key after checking it with the service. */ public static function handle_connect() { self::require_admin(); check_admin_referer( 'walletpb_connect' ); $key = isset( $_POST['walletpb_api_key'] ) ? trim( sanitize_text_field( wp_unslash( $_POST['walletpb_api_key'] ) ) ) : ''; if ( '' === $key ) { self::notice( 'error', __( 'Paste your API key first.', 'walletpassbuilder' ) ); } else { $account = WalletPB_API::account( $key ); if ( is_wp_error( $account ) ) { self::notice( 'error', $account->get_error_message() ); } else { update_option( WalletPB_API::OPTION_KEY, $key, false ); delete_transient( 'walletpb_pass_choices' ); self::notice( 'success', __( 'Connected to WalletPassBuilder.', 'walletpassbuilder' ) ); } } wp_safe_redirect( self::url( 'pass' ) ); exit; } /** * Removes the API key. */ public static function handle_disconnect() { self::require_admin(); check_admin_referer( 'walletpb_disconnect' ); delete_option( WalletPB_API::OPTION_KEY ); delete_transient( 'walletpb_pass_choices' ); self::notice( 'success', __( 'Disconnected. Buttons already on your pages keep working.', 'walletpassbuilder' ) ); wp_safe_redirect( self::url( 'pass' ) ); exit; } /** * Each booking template's data fields and sample values, for the editor's "+ Data Field" menu * and preview. * * @return array */ private static function template_fields() { $out = array(); foreach ( WalletPB_Integration::all() as $class ) { if ( $class::available() ) { $out[ $class::SOURCE ] = array( 'tokens' => $class::tokens(), 'sample' => $class::sample(), ); } } return $out; } /** * The booking template the editor shows (from ?template=), or "" for the Generic Pass. * * @param string $source Requested source; "booking" is LatePoint's older name. * @return string Class name. */ private static function integration_for( $source ) { $class = WalletPB_Integration::by_source( 'booking' === $source ? 'latepoint' : $source ); return $class && $class::available() ? $class : ''; } /** * Saves the pass template form, and creates or updates the pass when asked. */ public static function handle_save_template() { self::require_admin(); check_admin_referer( 'walletpb_save_template' ); $raw = isset( $_POST['walletpb'] ) && is_array( $_POST['walletpb'] ) ? map_deep( wp_unslash( $_POST['walletpb'] ), 'strval' ) : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized field by field in WalletPB_Template::sanitize() and WalletPB_Integration::sanitize_template(). $class = self::integration_for( isset( $_POST['walletpb_mode'] ) ? sanitize_key( wp_unslash( $_POST['walletpb_mode'] ) ) : '' ); if ( $class ) { self::save_booking_template( $raw, $class ); wp_safe_redirect( self::url( 'pass', array( 'template' => $class::SOURCE ) ) ); exit; } $values = WalletPB_Template::sanitize( $raw ); update_option( WalletPB_Template::OPTION_VALUES, $values, false ); if ( isset( $_POST['walletpb_publish'] ) ) { $result = WalletPB_Template::publish( __( 'Pass template', 'walletpassbuilder' ) ); if ( is_wp_error( $result ) ) { self::notice( 'error', $result->get_error_message() ); } else { $message = $result['created'] ? __( 'Pass created. Add the Add to Wallet block or shortcode to a page so people can save it.', 'walletpassbuilder' ) : __( 'Pass updated. Phones that saved it get the new version.', 'walletpassbuilder' ); // Booking passes share this brand (business name, logo, colour), so they follow along. $refreshed = 0; $failed = 0; foreach ( WalletPB_Integration::all() as $integration ) { if ( $integration::enabled() ) { is_wp_error( $integration::refresh_design() ) ? ++$failed : ++$refreshed; } } if ( $failed ) { $message .= ' ' . __( 'A pass could not be updated; see Activity.', 'walletpassbuilder' ); } elseif ( $refreshed ) { $message .= ' ' . __( 'Booking passes got the new look too.', 'walletpassbuilder' ); } self::notice( 'success', $message ); } } else { self::notice( 'success', __( 'Template saved.', 'walletpassbuilder' ) ); } wp_safe_redirect( self::url( 'pass' ) ); exit; } /** * Saves the booking template from the editor. The brand is shared: changes to it are saved * for the event pass too (and reach it the next time that pass is updated). * * @param array $raw Posted values. * @param string $class The integration. */ private static function save_booking_template( $raw, $class ) { $event = WalletPB_Template::values(); $brand = WalletPB_Template::sanitize( array_merge( $event, $raw ) ); foreach ( array( 'logo_text', 'logo_id', 'background_color' ) as $key ) { $event[ $key ] = $brand[ $key ]; } update_option( WalletPB_Template::OPTION_VALUES, $event, false ); $result = $class::save_template( $raw ); if ( is_wp_error( $result ) ) { self::notice( 'error', $result->get_error_message() ); } else { self::notice( 'success', $class::enabled() ? __( 'Pass saved. Passes already on phones are updating to it.', 'walletpassbuilder' ) /* translators: %s: the booking plugin, such as LatePoint. */ : sprintf( __( 'Pass saved. Turn %s on under Integrations to start issuing it.', 'walletpassbuilder' ), $class::label() ) ); } } /** * Sends the Help tab's message. On failure the message is kept, so it isn't typed again. */ public static function handle_support() { self::require_admin(); check_admin_referer( 'walletpb_support' ); // phpcs:disable WordPress.Security.NonceVerification.Missing -- checked above. $email = isset( $_POST['walletpb_email'] ) ? sanitize_email( wp_unslash( $_POST['walletpb_email'] ) ) : ''; $subject = isset( $_POST['walletpb_subject'] ) ? sanitize_text_field( wp_unslash( $_POST['walletpb_subject'] ) ) : ''; $message = isset( $_POST['walletpb_message'] ) ? sanitize_textarea_field( wp_unslash( $_POST['walletpb_message'] ) ) : ''; $details = ! empty( $_POST['walletpb_diagnostics'] ); $topic = isset( $_POST['walletpb_topic'] ) && 'integration' === sanitize_key( wp_unslash( $_POST['walletpb_topic'] ) ) ? 'integration' : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- each file is checked in WalletPB_Support::screenshots(). $files = isset( $_FILES['walletpb_screenshots'] ) && is_array( $_FILES['walletpb_screenshots'] ) ? $_FILES['walletpb_screenshots'] : array(); // phpcs:enable WordPress.Security.NonceVerification.Missing $subject = function_exists( 'mb_substr' ) ? mb_substr( $subject, 0, WalletPB_Support::MAX_SUBJECT ) : substr( $subject, 0, WalletPB_Support::MAX_SUBJECT ); $message = function_exists( 'mb_substr' ) ? mb_substr( $message, 0, WalletPB_Support::MAX_MESSAGE ) : substr( $message, 0, WalletPB_Support::MAX_MESSAGE ); $draft = 'walletpb_support_draft_' . get_current_user_id(); $shots = WalletPB_Support::screenshots( $files ); $result = is_wp_error( $shots ) ? $shots : WalletPB_Support::send( $email, $subject, $message, $details ? ( $topic ? $topic : 'general' ) : '', $shots ); if ( is_wp_error( $result ) ) { set_transient( $draft, compact( 'email', 'subject', 'message', 'details' ), HOUR_IN_SECONDS ); // The message is kept, but files can't be: say so when there were some. $again = $files && ! empty( array_filter( (array) ( isset( $files['name'] ) ? $files['name'] : array() ) ) ) ? ' ' . __( 'Attach your screenshots again before sending.', 'walletpassbuilder' ) : ''; self::notice( 'error', $result->get_error_message() . $again ); } else { delete_transient( $draft ); /* translators: %s: the email address the reply goes to. */ self::notice( 'success', sprintf( __( 'Thanks, your message is on its way. We\'ll reply to %s.', 'walletpassbuilder' ), $email ) ); } wp_safe_redirect( self::url( 'help', $topic && is_wp_error( $result ) ? array( 'topic' => $topic ) : array() ) ); exit; } /** * Retries a failed activity row. */ public static function handle_retry() { self::require_admin(); check_admin_referer( 'walletpb_retry' ); $id = isset( $_POST['walletpb_row'] ) ? absint( $_POST['walletpb_row'] ) : 0; $row = $id ? WalletPB_Activity::get( $id ) : null; if ( ! $row || 'failed' !== $row['status'] || empty( $row['request'] ) ) { self::notice( 'error', __( 'That entry can no longer be retried.', 'walletpassbuilder' ) ); } else { $request = json_decode( $row['request'], true ); if ( isset( $request['kind'] ) && 'card' === $request['kind'] ) { // A card: sync the booking (or other source) again as it is now. $class = WalletPB_Integration::by_source( (string) $request['source'] ); $result = $class ? $class::sync_id( $request['object_id'] ) : null; $card = WalletPB_Cards::get( $request['source'], $request['object_id'] ); if ( is_wp_error( $result ) ) { self::notice( 'error', $result->get_error_message() ); } elseif ( $card && 'ok' === $card['status'] ) { WalletPB_Activity::mark_retried( $id, __( 'Retried successfully.', 'walletpassbuilder' ), $card['pass_id'], $card['card_url'] ); self::notice( 'success', __( 'Retried successfully.', 'walletpassbuilder' ) ); } else { self::notice( 'error', __( 'That entry can no longer be retried.', 'walletpassbuilder' ) ); } } elseif ( isset( $request['kind'] ) && 'template' === $request['kind'] ) { // Retries use the template as it's saved now, so a fix made since the failure is included. $result = WalletPB_Template::publish( __( 'Retry', 'walletpassbuilder' ) ); if ( is_wp_error( $result ) ) { self::notice( 'error', $result->get_error_message() ); } else { WalletPB_Activity::mark_retried( $id, __( 'Retried successfully.', 'walletpassbuilder' ), $result['pass_id'], $result['pass_url'] ); self::notice( 'success', __( 'Retried successfully.', 'walletpassbuilder' ) ); } } } wp_safe_redirect( self::url( 'activity' ) ); exit; } /** * An integration: turn on (creating its booking passes the first time) or off. Booking passes' * design follows the Pass Template: they're updated whenever that pass is. */ public static function handle_integration() { self::require_admin(); check_admin_referer( 'walletpb_integration' ); $op = isset( $_POST['walletpb_op'] ) ? sanitize_key( wp_unslash( $_POST['walletpb_op'] ) ) : ''; $key = isset( $_POST['walletpb_host'] ) ? sanitize_key( wp_unslash( $_POST['walletpb_host'] ) ) : ''; $hosts = WalletPB_Integration::hosts(); if ( isset( $hosts[ $key ] ) ) { list( $label, $classes ) = $hosts[ $key ]; $info = self::integration_info( $key ); if ( 'enable' === $op ) { $error = null; foreach ( $classes as $class ) { $result = $class::enable(); if ( is_wp_error( $result ) ) { $error = $result; break; } } self::notice( $error ? 'error' : 'success', /* translators: %s: the booking plugin, such as LatePoint. */ $error ? $error->get_error_message() : sprintf( ! empty( $info['on'] ) ? $info['on'] : __( '%s is on. New bookings get a wallet pass.', 'walletpassbuilder' ), $label ) ); } elseif ( 'disable' === $op ) { foreach ( $classes as $class ) { $class::disable(); } /* translators: %s: the booking plugin, such as LatePoint. */ self::notice( 'success', sprintf( ! empty( $info['off'] ) ? $info['off'] : __( '%s is off. New bookings no longer get a pass; passes already issued keep working.', 'walletpassbuilder' ), $label ) ); } } wp_safe_redirect( self::url( 'integrations' ) ); exit; } /** * Saves the Settings tab. */ public static function handle_save_settings() { self::require_admin(); check_admin_referer( 'walletpb_save_settings' ); update_option( self::OPTION_CLEANUP, isset( $_POST['walletpb_delete_data'] ) ? '1' : '0', false ); self::notice( 'success', __( 'Settings saved.', 'walletpassbuilder' ) ); wp_safe_redirect( self::url( 'settings' ) ); exit; } /* ---------- page ---------- */ /** * The admin page. */ public static function page() { if ( ! current_user_can( 'manage_options' ) ) { return; } $tabs = array( 'pass' => __( 'Pass Template', 'walletpassbuilder' ), 'button' => __( 'Add to Wallet button', 'walletpassbuilder' ), 'integrations' => __( 'Integrations', 'walletpassbuilder' ), 'activity' => __( 'Activity', 'walletpassbuilder' ), 'settings' => __( 'Settings', 'walletpassbuilder' ), 'help' => __( 'Help', 'walletpassbuilder' ), ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- only picks which tab to show. $tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : ''; if ( ! isset( $tabs[ $tab ] ) ) { $tab = 'pass'; } echo '
'; echo '
'; echo '

' . esc_html__( 'WalletPassBuilder', 'walletpassbuilder' ) . '

'; echo WalletPB_API::is_connected() ? '' . esc_html__( 'Connected', 'walletpassbuilder' ) . '' : '' . esc_html__( 'Not connected', 'walletpassbuilder' ) . ''; echo '

'; self::render_notice(); echo ''; call_user_func( array( __CLASS__, 'tab_' . $tab ) ); echo '
'; } /** * Shows a "connect first" box when there's no API key, and says whether it did. * * @return bool True when connected. */ private static function require_connection() { if ( WalletPB_API::is_connected() ) { return true; } echo '

' . esc_html__( 'Connect your WalletPassBuilder account first.', 'walletpassbuilder' ) . '

'; echo '

' . esc_html__( 'Connect account', 'walletpassbuilder' ) . '

'; return false; } /** * The connect form, shown on the Pass Template tab until an account is connected. */ private static function render_connect() { echo '
'; echo '

' . esc_html__( 'Connect your account', 'walletpassbuilder' ) . '

'; echo '

' . wp_kses( sprintf( /* translators: 1: sign-up link, 2: API key page link. */ __( 'You need a WalletPassBuilder account (create one free). Then copy your API key from API in the dashboard and paste it here.', 'walletpassbuilder' ), esc_url( 'https://walletpassbuilder.com/signup' ), esc_url( 'https://walletpassbuilder.com/api-keys' ) ), array( 'a' => array( 'href' => array(), 'target' => array(), 'rel' => array(), ), ) ) . '

'; echo '
'; wp_nonce_field( 'walletpb_connect' ); echo ''; echo '


'; echo '

'; submit_button( __( 'Connect', 'walletpassbuilder' ), 'primary', 'submit', false ); echo '
'; echo '
'; } /** * The connected account, with Disconnect - on the Settings tab. */ private static function render_account() { echo '

' . esc_html__( 'Account', 'walletpassbuilder' ) . '

'; if ( ! WalletPB_API::is_connected() ) { echo '

' . esc_html__( 'Not connected.', 'walletpassbuilder' ) . ' ' . esc_html__( 'Connect your account', 'walletpassbuilder' ) . '

'; return; } $account = WalletPB_API::account(); if ( is_wp_error( $account ) ) { echo '

' . esc_html( $account->get_error_message() ) . '

'; } else { $credits = WalletPB_API::credits(); echo '
'; self::info_row( __( 'Account', 'walletpassbuilder' ), trim( ( isset( $account['name'] ) ? $account['name'] : '' ) . ( ! empty( $account['email'] ) ? ' (' . $account['email'] . ')' : '' ) ) ); self::info_row( __( 'Plan', 'walletpassbuilder' ), ! empty( $account['plan'] ) ? ucfirst( (string) $account['plan'] ) : __( 'Free', 'walletpassbuilder' ) ); $has_credits = ! is_wp_error( $credits ) && isset( $credits['remaining'] ); echo '
' . esc_html__( 'Pass credits left', 'walletpassbuilder' ) . '
'; echo ''; echo esc_html( $has_credits /* translators: 1: credits left, 2: total credits. */ ? sprintf( __( '%1$s of %2$s', 'walletpassbuilder' ), number_format_i18n( (int) $credits['remaining'] ), number_format_i18n( (int) $credits['balance'] ) ) : __( 'Not available', 'walletpassbuilder' ) ); echo ''; // Accounts billed under a contract buy nothing in the app, so there's nowhere to send them. if ( empty( $account['managedBilling'] ) ) { echo ' ' . esc_html__( 'Buy credits or upgrade', 'walletpassbuilder' ) . ' ↗'; } echo '
'; self::info_row( __( 'API key', 'walletpassbuilder' ), self::masked_key() ); echo '
'; echo '

' . esc_html__( 'Passes are created in your account\'s default workspace. Change it in WalletPassBuilder under Settings > Workspace.', 'walletpassbuilder' ) . '

'; } echo '
'; wp_nonce_field( 'walletpb_disconnect' ); echo ''; submit_button( __( 'Disconnect', 'walletpassbuilder' ), 'secondary', 'submit', false ); echo '
'; } /** * One label and value row. * * @param string $label Label. * @param string $value Value. */ private static function info_row( $label, $value ) { echo '
' . esc_html( $label ) . '
' . esc_html( $value ) . '
'; } /** * A page in the WalletPassBuilder web app, through sign-in if needed. * * @param string $path App path, such as "/home". * @param string $campaign Campaign tag for where the link was clicked. * @return string */ private static function app_url( $path, $campaign ) { // The campaign tags ride on the page it lands on: a signed-in visitor skips straight past /login. $next = $path . '?utm_source=wordpress&utm_medium=plugin&utm_campaign=' . rawurlencode( $campaign ); return 'https://walletpassbuilder.com/login?next=' . rawurlencode( $next ); } /** * The saved key with only its start and end shown. * * @return string */ private static function masked_key() { $key = WalletPB_API::api_key(); return strlen( $key ) > 14 ? substr( $key, 0, 9 ) . str_repeat( '•', 8 ) . substr( $key, -4 ) : str_repeat( '•', 8 ); } /** * Pass Template tab: the connect form until an account is connected, then the template. */ private static function tab_pass() { if ( ! WalletPB_API::is_connected() ) { self::render_connect(); return; } // The templates share this editor and the brand: the Generic Pass (the shortcode's pass) and, // for each booking plugin installed, its booking passes (a card per booking). // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- only picks which template to show. $class = self::integration_for( isset( $_GET['template'] ) ? sanitize_key( wp_unslash( $_GET['template'] ) ) : '' ); $booking = '' !== $class; $v = $booking ? array_merge( WalletPB_Template::values(), $class::booking_template() ) : WalletPB_Template::values(); $pass = $booking ? array() : WalletPB_Template::pass(); $logo = $v['logo_id'] ? wp_get_attachment_image_url( $v['logo_id'], 'thumbnail' ) : ''; $logo = $logo ? $logo : WalletPB_Template::default_logo_url(); $name = function ( $field ) { return 'walletpb[' . $field . ']'; }; $templates = array_filter( WalletPB_Integration::all(), function ( $integration ) { return $integration::available(); } ); if ( $templates ) { echo ''; } $mode = $booking ? $class::SOURCE : 'event'; $host_info = $booking ? self::integration_info( WalletPB_Integration::host_of( $class::SOURCE ) ) : null; echo '
'; wp_nonce_field( 'walletpb_save_template' ); echo ''; echo '
'; if ( $booking ) { $issued = WalletPB_Cards::count( $class::SOURCE ); echo $class::enabled() ? '
' . esc_html( ! empty( $host_info['banner'] ) ? $host_info['banner'] : __( 'Every new booking or ticket gets its own card on this pass.', 'walletpassbuilder' ) ) . ' ' /* translators: %s: number of passes. */ . esc_html( sprintf( _n( '%s issued so far.', '%s issued so far.', $issued, 'walletpassbuilder' ), number_format_i18n( $issued ) ) ) . ' ' . esc_html__( 'Use "+ Data Field" to put its details on it.', 'walletpassbuilder' ) . '
' /* translators: %s: the booking plugin, such as LatePoint. */ : '
' . esc_html( sprintf( __( '%s is off.', 'walletpassbuilder' ), $class::label() ) ) . ' ' . esc_html__( 'Design the pass here, then turn it on under Integrations.', 'walletpassbuilder' ) . '
' . esc_html__( 'Integrations', 'walletpassbuilder' ) . '
'; } if ( $pass ) { echo '
' . esc_html__( 'Your pass is live.', 'walletpassbuilder' ) . ' '; echo esc_html__( 'Show it with the Add to Wallet block or', 'walletpassbuilder' ) . ' [wallet_pass_builder]
'; echo '' . esc_html__( 'Open pass page', 'walletpassbuilder' ) . '
'; } // Brand: shared by every template. self::panel_open( __( 'Brand', 'walletpassbuilder' ), __( 'How your business appears at the top of every pass.', 'walletpassbuilder' ), 'walletpb-brand' ); // One row: name, logo, colour. The IDs and classes the script uses are unchanged. echo '
'; echo '
'; echo '
'; echo ''; echo '
'; echo '
'; echo ''; echo ''; echo ''; foreach ( array( '#065f46', '#111827', '#1e3a8a', '#7c2d12', '#9d174d', '#6d28d9', '#f5d76e', '#f3f4f6' ) as $swatch ) { echo ''; } echo '
'; echo '
'; echo '' . esc_html__( 'A square PNG logo works best. Text on the pass switches between black and white, whichever reads best.', 'walletpassbuilder' ) . ''; self::panel_close(); // Front. self::panel_open( __( 'Front', 'walletpassbuilder' ), __( 'The title and up to three short fields people see at a glance.', 'walletpassbuilder' ) ); self::text_field( $name( 'header' ), __( 'Header', 'walletpassbuilder' ), isset( $v['header'] ) ? $v['header'] : '', 'header', $booking ? __( 'A short line above the title, such as the ticket type. Optional.', 'walletpassbuilder' ) : __( 'A short line above the title, such as "General Admission". Optional.', 'walletpassbuilder' ), '', 30 ); self::text_field( $name( 'card_title' ), __( 'Pass title', 'walletpassbuilder' ), $v['card_title'], 'card_title', $booking ? sprintf( /* translators: %s: a data field, such as {service}. */ __( 'For example %s.', 'walletpassbuilder' ), '{' . (string) key( $class::tokens() ) . '}' ) : __( 'For example the event name.', 'walletpassbuilder' ) ); echo '
' . esc_html__( 'Fields', 'walletpassbuilder' ) . '
'; foreach ( $v['fields'] as $i => $field ) { echo '
'; echo ''; echo ''; echo '
'; } echo '
'; self::panel_close(); // Barcode. self::panel_open( __( 'Barcode', 'walletpassbuilder' ), __( 'Scanned at the door or the counter.', 'walletpassbuilder' ) ); echo '
'; foreach ( array( 'qr' => __( 'QR code', 'walletpassbuilder' ), 'barcode' => __( 'Barcode', 'walletpassbuilder' ), 'none' => __( 'None', 'walletpassbuilder' ), ) as $value => $label ) { echo ''; } echo '
'; self::text_field( $name( 'scan_value' ), __( 'Value', 'walletpassbuilder' ), $v['scan_value'], 'scan_value', __( 'A ticket number or a link.', 'walletpassbuilder' ), 'walletpb-scan' ); self::text_field( $name( 'scan_label' ), __( 'Text under the code', 'walletpassbuilder' ), $v['scan_label'], 'scan_label', __( 'Optional.', 'walletpassbuilder' ), 'walletpb-scan' ); echo '
'; self::panel_close(); // Back. self::panel_open( __( 'Back', 'walletpassbuilder' ), __( 'Details people see when they open the pass, such as directions, the refund policy or a phone number.', 'walletpassbuilder' ) ); echo '
'; $sections = $v['back'] ? $v['back'] : array( array( 'type' => 'Text', 'label' => '', 'value' => '', ), ); foreach ( $sections as $i => $section ) { self::back_row( $i, $section ); } echo '
'; echo ''; echo ''; self::panel_close(); self::advanced_panel( $v, $name, $booking, $booking ? $class::tokens() : array() ); echo '
'; echo '
'; echo '
'; if ( $booking ) { if ( $class::enabled() ) { echo esc_html( ! empty( $host_info['save_on'] ) ? $host_info['save_on'] : __( 'Saving updates every booking pass, including ones already on phones. Each new booking uses one pass credit.', 'walletpassbuilder' ) ); } else { /* translators: %s: the booking plugin, such as LatePoint. */ echo esc_html( sprintf( ! empty( $host_info['save_off'] ) ? $host_info['save_off'] : __( 'Saved for when you turn %s on. Each booking then uses one pass credit.', 'walletpassbuilder' ), $class::label() ) ); } echo '
'; echo ''; } else { echo $pass ? esc_html__( 'Updating sends the new version to every phone that saved the pass.', 'walletpassbuilder' ) : esc_html__( 'Creating the pass uses one pass credit.', 'walletpassbuilder' ); echo '
'; echo ''; echo ''; } echo '
'; } /** * The booking details a hero image variation can match on: the ones many bookings share * (ticket type, service, status, staff), not dates, times, names or codes, which differ for * nearly every booking. * * @param array $tokens The booking details, as {token} => label. * @return array */ private static function variation_tokens( $tokens ) { $unique = array( 'date', 'start_time', 'end_time', 'duration', 'starts_at', 'booking_code', 'booking_id', 'ticket_code', 'order_number', 'customer_name', 'attendee_name', 'first_name' ); return array_diff_key( $tokens, array_flip( $unique ) ); } /** * A variation's condition as a short sentence: "Ticket type is Kids". * * @param array $rule variable, match, value. * @param array $tokens The booking details, as {token} => label. * @return string HTML. */ private static function variation_label( $rule, $tokens ) { $matches = self::variation_matches(); $field = isset( $tokens[ $rule['variable'] ] ) ? $tokens[ $rule['variable'] ] : $rule['variable']; $how = isset( $matches[ $rule['match'] ] ) ? $matches[ $rule['match'] ] : $matches['is']; $value = '' === $rule['value'] ? '' . esc_html__( 'empty', 'walletpassbuilder' ) . '' : '' . esc_html( $rule['value'] ) . ''; return esc_html( $field . ' ' . $how ) . ' ' . $value; } /** * How a variation compares a booking's value, by key. * * @return array */ private static function variation_matches() { return array( 'is' => __( 'is', 'walletpassbuilder' ), 'isNot' => __( 'is not', 'walletpassbuilder' ), 'contains' => __( 'contains', 'walletpassbuilder' ), ); } /** * One hero image card: the image, and under it who gets it. * * @param string $i The variation's index in the form ("__i__" in the template), or * "" for the main image. * @param array $rule image_id, crop, variable, match, value (a variation), or the main * image's image_id and crop. * @param callable $name Field name builder. * @param array $tokens The booking details a variation can match on. * @param bool $booking Whether it's a booking template. */ private static function hero_card( $i, $rule, $name, $tokens, $booking ) { $main = '' === $i; $rule = wp_parse_args( $rule, array( 'image_id' => 0, 'crop' => array(), 'variable' => (string) key( $tokens ), 'match' => 'is', 'value' => '', ) ); $url = $rule['image_id'] ? wp_get_attachment_image_url( (int) $rule['image_id'], 'large' ) : ''; $crop = WalletPB_Advanced::sanitize_crop( $rule['crop'] ); $field = function ( $key ) use ( $name, $i ) { return $name( 'image_rules' ) . '[' . $i . '][' . $key . ']'; }; echo '
  • '; if ( $main ) { echo ''; echo ''; } else { echo ''; echo ''; echo ''; echo ''; echo ''; } $label = $main ? ( $booking ? __( 'Everyone else', 'walletpassbuilder' ) : __( 'Every pass', 'walletpassbuilder' ) ) : ''; echo '
  • '; } /** * Advanced: the wide image, and when Wallet reminds people of the pass. * * @param array $v Template values. * @param callable $name Field name builder. * @param bool $booking Whether it's a booking template. * @param array $tokens The booking details, as {token} => label (bookings only). */ private static function advanced_panel( $v, $name, $booking, $tokens = array() ) { $v = wp_parse_args( $v, WalletPB_Advanced::defaults() ); self::panel_open( __( 'Advanced', 'walletpassbuilder' ), __( 'A hero image, and when Wallet reminds people about the pass.', 'walletpassbuilder' ), 'walletpb-advanced' ); // The hero image as a row of cards: the main image, then (bookings) each variation in the // order they're checked, then "+ Add a variation". A card opens the card editor below. $tokens = $booking ? self::variation_tokens( $tokens ) : array(); $rules = $booking && $tokens && is_array( $v['image_rules'] ) ? array_values( $v['image_rules'] ) : array(); echo '
    ' . esc_html__( 'Hero image', 'walletpassbuilder' ) . ''; echo '
      '; self::hero_card( '', array( 'image_id' => $v['strip_id'], 'crop' => $v['strip_crop'], ), $name, $tokens, $booking ); foreach ( $rules as $i => $rule ) { self::hero_card( (string) $i, $rule, $name, $tokens, $booking ); } if ( $booking && $tokens ) { echo '
    1. '; } echo '
    '; if ( $booking && $tokens ) { echo ''; } echo '' . esc_html( $booking && $tokens ? __( 'Shown across the pass on iPhone and at the bottom on Android. Add variations for bookings such as kids\' tickets or cancelled ones: each booking gets the first one it matches, in number order (drag to reorder).', 'walletpassbuilder' ) : __( 'Shown across the pass on iPhone and at the bottom on Android. Any image works: you move and zoom it to fit the strip.', 'walletpassbuilder' ) ) . '
    '; // The card editor: one card's image, and for a variation, the bookings that get it. echo ''; echo '

    '; echo '
    '; echo ''; echo '
    '; if ( $tokens ) { $matches = self::variation_matches(); echo '
    ' . esc_html__( 'Use it for bookings where', 'walletpassbuilder' ) . '
    '; echo '
    '; echo '' . esc_html__( 'Capital letters and spaces at either end don\'t matter.', 'walletpassbuilder' ) . '
    '; } echo '
    '; echo ''; echo ''; echo '
    '; echo '
    '; // Moving and zooming the image inside the strip. echo ''; echo '

    ' . esc_html__( 'Adjust the hero image', 'walletpassbuilder' ) . '

    '; echo '

    ' . esc_html__( 'Drag to move it and zoom to crop. What\'s inside the frame is what the pass shows.', 'walletpassbuilder' ) . '

    '; echo '
    '; echo ''; echo '
    '; echo ''; echo '
    '; echo '
    '; echo '
    ' . esc_html__( 'Reminders', 'walletpassbuilder' ) . '
    '; // At the start time. echo '
    '; echo '
    '; if ( $booking ) { echo '' . esc_html__( 'iPhone shows the pass on the lock screen at each booking\'s start time; Google Wallet sends a reminder the day before.', 'walletpassbuilder' ) . ''; } else { echo ''; $zone = wp_timezone_string(); /* translators: %s: the site's time zone, such as Europe/London. */ echo '' . esc_html( sprintf( __( 'In your site\'s time zone (%s). iPhone shows the pass on the lock screen at this time; Google Wallet sends a reminder the day before.', 'walletpassbuilder' ), in_array( $zone, array( '+00:00', 'UTC' ), true ) ? 'UTC' : $zone ) ) . ''; } echo '
    '; // Before it expires (the Generic Pass; a booking's pass ends with the booking). if ( ! $booking ) { echo '
    '; echo '
    '; echo ''; echo '' . esc_html__( 'Both wallets remind people 48 hours before, and the pass is marked expired at this time. With the start reminder on too, iPhone uses that one and Google Wallet uses this one.', 'walletpassbuilder' ) . ''; echo '
    '; } // Near a place. echo '
    '; echo '
    '; echo '
      '; foreach ( $v['locations'] as $i => $place ) { self::place_row( $i, $place ); } echo '
    '; echo ''; /* translators: %d: how many places. */ echo '' . esc_html( sprintf( __( 'Up to %d places. The pass comes up on the lock screen nearby.', 'walletpassbuilder' ), WalletPB_Advanced::MAX_LOCATIONS ) ) . ''; echo ''; echo ''; echo '
    '; echo '
    '; self::panel_close(); } /** * One chosen place. * * @param int|string $i Index. * @param array $place address, latitude, longitude. */ private static function place_row( $i, $place ) { $field = function ( $key ) use ( $i ) { return 'walletpb[locations][' . $i . '][' . $key . ']'; }; echo '
  • ' . esc_html( $place['address'] ) . ''; foreach ( array( 'address', 'latitude', 'longitude' ) as $key ) { echo ''; } echo '
  • '; } /** * What the address search shows when a lookup fails. A 404 means the service doesn't have * address search (an older service), so it says that instead of the bare status. * * @param WP_Error $error The failed request. * @return string */ private static function places_error( $error ) { $data = $error->get_error_data(); if ( is_array( $data ) && isset( $data['status'] ) && 404 === (int) $data['status'] ) { return __( 'Address search isn\'t available on WalletPassBuilder yet. Try again later.', 'walletpassbuilder' ); } return $error->get_error_message(); } /** * Address suggestions for the editor, from the service with the site's API key (the key * never reaches the browser). */ public static function ajax_places() { check_ajax_referer( 'walletpb_places', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( null, 403 ); } $q = isset( $_GET['q'] ) ? sanitize_text_field( wp_unslash( $_GET['q'] ) ) : ''; $session = isset( $_GET['session'] ) ? sanitize_key( wp_unslash( $_GET['session'] ) ) : ''; $result = WalletPB_API::request( 'GET', '/places?' . http_build_query( array( 'q' => $q, 'session' => $session ) ) ); if ( is_wp_error( $result ) ) { wp_send_json_error( array( 'message' => self::places_error( $result ) ) ); } wp_send_json_success( isset( $result['places'] ) ? $result['places'] : array() ); } /** * A picked suggestion's address and coordinates. */ public static function ajax_place() { check_ajax_referer( 'walletpb_places', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( null, 403 ); } $id = isset( $_GET['id'] ) ? sanitize_text_field( wp_unslash( $_GET['id'] ) ) : ''; $session = isset( $_GET['session'] ) ? sanitize_key( wp_unslash( $_GET['session'] ) ) : ''; $result = WalletPB_API::request( 'GET', '/places/' . rawurlencode( $id ) . '?' . http_build_query( array( 'session' => $session ) ) ); if ( is_wp_error( $result ) ) { wp_send_json_error( array( 'message' => self::places_error( $result ) ) ); } wp_send_json_success( $result ); } /** * Template values with every {booking detail} filled from a sample appointment, for the preview. * * @param array $v Template values. * @param string $class The integration. * @return array */ private static function with_sample( $v, $class ) { $sample = $class::sample(); $fill = function ( $text ) use ( $class, $sample ) { return $class::fill( $text, $sample ); }; $v['header'] = $fill( isset( $v['header'] ) ? $v['header'] : '' ); $v['card_title'] = $fill( $v['card_title'] ); $v['scan_value'] = $fill( $v['scan_value'] ); $v['scan_label'] = $fill( $v['scan_label'] ); foreach ( $v['fields'] as $i => $field ) { $v['fields'][ $i ] = array( 'label' => $fill( $field['label'] ), 'value' => $fill( $field['value'] ), ); } return $v; } /** * Opens an editor panel. * * @param string $title Heading. * @param string $help Line under the heading. * @param string $class Extra class. */ private static function panel_open( $title, $help, $class = '' ) { echo '

    ' . esc_html( $title ) . '

    ' . esc_html( $help ) . '

    '; } /** * Closes an editor panel. */ private static function panel_close() { echo '
    '; } /** * A labelled text input. * * @param string $name Field name. * @param string $label Label. * @param string $value Value. * @param string $preview Preview key. * @param string $help Help text. * @param string $class Extra input class. */ private static function text_field( $name, $label, $value, $preview, $help = '', $class = '', $max = 0 ) { $id = 'walletpb-' . $preview; echo '
    '; echo ''; if ( '' !== $help ) { echo '' . esc_html( $help ) . ''; } echo '
    '; } /** * One back section's inputs. * * @param int|string $i Index. * @param array $section type, label and value. */ private static function back_row( $i, $section ) { $base = 'walletpb[back][' . $i . ']'; echo '
    '; echo ''; echo ''; echo ''; echo ''; echo '
    '; } /** * The live previews: the pass as Apple Wallet and as Google Wallet show it, with real codes * (drawn by admin.js). * * @param array $v Template values. * @param string $logo Logo thumbnail URL. * @param string $note The line under the preview, or "" for the usual one. */ private static function preview( $v, $logo, $note = '' ) { $initial = function_exists( 'mb_substr' ) ? mb_substr( $v['logo_text'], 0, 1 ) : substr( $v['logo_text'], 0, 1 ); $logo_el = function ( $class ) use ( $logo, $initial ) { echo ''; echo ''; echo '' . esc_html( $initial ) . ''; echo ''; }; $fields = function ( $class ) use ( $v ) { echo '
    '; foreach ( $v['fields'] as $i => $field ) { echo '
    ' . esc_html( $field['label'] ) . '
    '; echo '
    ' . esc_html( $field['value'] ) . '
    '; } echo '
    '; }; $code = function () use ( $v ) { echo '
    '; echo '
    ' . esc_html( $v['scan_label'] ) . '
    '; }; echo '
    '; // Which wallet and which side to show - Apple Wallet's front first, like the builder. echo '
    '; echo '
    '; echo ''; echo '
    '; echo '
    '; echo ''; echo '
    '; echo '
    '; echo '
    '; $strip_url = ! empty( $v['strip_id'] ) ? wp_get_attachment_image_url( (int) $v['strip_id'], 'large' ) : ''; $strip_crop = WalletPB_Advanced::crop_css( WalletPB_Advanced::sanitize_crop( isset( $v['strip_crop'] ) ? $v['strip_crop'] : '' ) ); $strip = function ( $class ) use ( $strip_url, $strip_crop ) { echo '
    '; }; // Apple Wallet, front. $ink = WalletPB_Template::readable_on( $v['background_color'] ); printf( '
    ', esc_attr( $v['background_color'] ), esc_attr( $ink ) ); echo '
    '; $logo_el( 'walletpb-apple-logo' ); echo '' . esc_html( $v['logo_text'] ) . '
    '; $strip( 'walletpb-apple-strip' ); echo '
    ' . esc_html( isset( $v['header'] ) ? $v['header'] : '' ) . '
    '; echo '
    ' . esc_html( $v['card_title'] ) . '
    '; $fields( 'walletpb-apple-fields' ); $code(); echo '
    '; // Apple Wallet, back: Wallet's pass details screen. echo '
    '; printf( '
    ', esc_attr( $v['background_color'] ), esc_attr( $ink ) ); echo '
    '; $logo_el( 'walletpb-apple-logo' ); echo '' . esc_html( $v['logo_text'] ) . '
    '; echo '
    ' . esc_html( isset( $v['header'] ) ? $v['header'] : '' ) . '
    '; echo '
    ' . esc_html( $v['card_title'] ) . '
    '; echo '
    ' . esc_html( $v['card_title'] ) . '
    '; echo '
    ' . esc_html__( 'Automatic Updates', 'walletpassbuilder' ) . '
    '; echo '
    ' . esc_html__( 'Allow Notifications', 'walletpassbuilder' ) . '
    '; echo '
    ' . esc_html__( 'Remove Pass', 'walletpassbuilder' ) . '
    '; echo '
    '; echo '
    '; // Google Wallet, front. printf( '
    ', esc_attr( $v['background_color'] ), esc_attr( $ink ) ); $logo_el( 'walletpb-google-logo' ); echo '
    ' . esc_html( $v['logo_text'] ) . '
    '; echo '
    ' . esc_html( isset( $v['header'] ) ? $v['header'] : '' ) . '
    '; echo '
    ' . esc_html( $v['card_title'] ) . '
    '; $code(); $fields( 'walletpb-google-fields' ); $strip( 'walletpb-google-hero' ); echo '
    '; // Google Wallet, back: the Details sheet. echo '
    '; echo '
    ' . esc_html__( 'Details', 'walletpassbuilder' ) . '
    '; $logo_el( 'walletpb-gd-logo' ); echo '
    ' . esc_html( $v['card_title'] ) . '
    '; echo '
    '; echo '

    ' . esc_html__( 'The pass provider or merchant is responsible for the info on this pass and may send you notifications.', 'walletpassbuilder' ) . '

    '; echo '
    '; echo '

    ' . esc_html( '' !== $note ? $note : __( 'A close guide to how each wallet draws the pass.', 'walletpassbuilder' ) ) . '

    '; } /** * Add to Wallet button tab. */ private static function tab_button() { if ( ! self::require_connection() ) { return; } $template = WalletPB_Template::pass(); echo '
    '; echo '

    ' . esc_html__( 'Add the buttons to a page', 'walletpassbuilder' ) . '

    '; echo '

    ' . esc_html__( 'Visitors see the official Add to Apple Wallet and Add to Google Wallet buttons. Each one opens that wallet with the pass.', 'walletpassbuilder' ) . '

    '; echo WalletPB_Button::badges( $template ? untrailingslashit( $template['pass_url'] ) : '#', 'both', 'left' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in WalletPB_Button::badges(). echo '

    ' . esc_html__( 'In the block editor, add the "Add to Wallet" block and pick a pass in its settings. Anywhere else, use a shortcode:', 'walletpassbuilder' ) . '

    '; echo '
      '; echo '
    • [wallet_pass_builder] ' . esc_html__( 'the pass from the Pass Template tab.', 'walletpassbuilder' ) . '
    • '; echo '
    • [wallet_pass_builder pass="PASS_ID"] ' . esc_html__( 'any pass from your account (listed below).', 'walletpassbuilder' ) . '
    • '; echo '
    • wallets="apple" ' . esc_html__( 'or', 'walletpassbuilder' ) . ' wallets="google" ' . esc_html__( 'shows just one of the buttons;', 'walletpassbuilder' ) . ' align="center" ' . esc_html__( 'or', 'walletpassbuilder' ) . ' align="right" ' . esc_html__( 'moves them.', 'walletpassbuilder' ) . '
    • '; echo '
    '; $passes = WalletPB_Button::pass_choices(); echo '

    ' . esc_html__( 'Your passes', 'walletpassbuilder' ) . '

    '; echo '' . esc_html__( 'Manage passes in WalletPassBuilder', 'walletpassbuilder' ) . ' ↗
    '; if ( is_wp_error( $passes ) ) { echo '

    ' . esc_html( $passes->get_error_message() ) . '

    '; return; } if ( ! $passes ) { echo '

    ' . esc_html__( 'No live passes yet. Create one on the Pass Template tab, or in WalletPassBuilder.', 'walletpassbuilder' ) . '

    '; return; } echo ''; foreach ( $passes as $pass ) { $code = '[wallet_pass_builder pass="' . $pass['id'] . '"]'; echo ''; echo ''; } echo '
    ' . esc_html__( 'Pass', 'walletpassbuilder' ) . '' . esc_html__( 'Shortcode', 'walletpassbuilder' ) . '
    ' . esc_html( '' !== $pass['title'] ? $pass['title'] : $pass['id'] ) . '' . esc_html( $code ) . '
    '; } /** * What each integration says about itself: a one-line summary, where customers get their * pass without any setup, the email tag (the one manual step), and the longer story. * * @param string $host Host plugin key. * @return array tagline, slug, auto (list), email (tag, link, where) or null, how; optionally * todo (a setup step), idle, on and off (notices), and the editor's banner, * save_on, save_off, save_button and confirm. */ private static function integration_info( $host ) { $updates = __( 'When it changes, the pass on the customer\'s phone updates; if it is cancelled, the pass says so.', 'walletpassbuilder' ); // WooCommerce issues nothing until a product has "Wallet pass" ticked. $woo_none = 'woocommerce' === $host && class_exists( 'WalletPB_WooCommerce' ) && WalletPB_WooCommerce::available() && 0 === WalletPB_WooCommerce::ticked_products(); // EventON shows the box by itself unless the site arranged its event card without it. $evo_box = 'eventon' === $host && class_exists( 'WalletPB_EventON' ) && WalletPB_EventON::box_missing(); $info = array( 'latepoint' => array( 'tagline' => __( 'A pass for every appointment', 'walletpassbuilder' ), 'slug' => 'latepoint', 'auto' => array( __( 'Booking confirmation', 'walletpassbuilder' ), __( 'The customer\'s bookings', 'walletpassbuilder' ) ), 'email' => array( 'tag' => '{{wallet_pass_buttons}}', 'link' => '{{wallet_pass_link}}', 'where' => __( 'LatePoint › Settings › Notifications', 'walletpassbuilder' ), ), 'how' => __( 'Each appointment gets its own pass with the service, date, time, place and booking code.', 'walletpassbuilder' ) . ' ' . $updates, ), 'amelia' => array( 'tagline' => __( 'A pass for every appointment and event booking', 'walletpassbuilder' ), 'slug' => 'ameliabooking', 'auto' => array( __( 'Booking confirmation', 'walletpassbuilder' ) ), 'email' => array( 'tag' => '%wallet_pass_buttons%', 'link' => '%wallet_pass_link%', 'where' => __( 'Amelia › Notifications', 'walletpassbuilder' ), ), 'how' => __( 'Appointments and event bookings each get their own pass, with separate designs.', 'walletpassbuilder' ) . ' ' . $updates, ), 'sugar_calendar' => array( 'tagline' => __( 'A pass for every ticket', 'walletpassbuilder' ), 'slug' => 'sugar-calendar-lite', 'auto' => array( __( 'Order receipt', 'walletpassbuilder' ) ), 'email' => array( 'tag' => '{wallet_pass_buttons}', 'link' => '{wallet_pass_link}', 'where' => __( 'Sugar Calendar › Settings › Emails', 'walletpassbuilder' ), ), 'how' => __( 'Tickets bought together each get their own pass, with the ticket code as a QR code, so they can be shared and scanned one by one. A pass is issued once the order is paid; free tickets count as paid.', 'walletpassbuilder' ) . ' ' . $updates, ), 'event_tickets' => array( 'tagline' => __( 'A pass for every ticket and RSVP', 'walletpassbuilder' ), 'slug' => 'event-tickets', 'auto' => array( __( 'Order confirmation', 'walletpassbuilder' ), __( '"My Tickets" page', 'walletpassbuilder' ), __( 'Ticket emails', 'walletpassbuilder' ) ), 'email' => null, 'how' => __( 'Works with RSVPs and Tickets Commerce, on The Events Calendar\'s events or any page with tickets. A pass is issued once a ticket is paid for or an RSVP is "Going", and check-ins show on it.', 'walletpassbuilder' ) . ' ' . $updates, ), 'bookly' => array( 'tagline' => __( 'A pass for every appointment', 'walletpassbuilder' ), 'slug' => 'bookly-responsive-appointment-booking-tool', 'auto' => array( __( 'Booking form\'s last step', 'walletpassbuilder' ) ), 'email' => array( 'tag' => '[wallet_pass_buttons {booking_number}]', 'link' => '[wallet_pass_link {booking_number}]', 'where' => __( 'Bookly › Email notifications, with Bookly\'s mail gateway set to WordPress (not SMTP)', 'walletpassbuilder' ), ), 'how' => __( 'Each customer\'s booking gets its own pass, so everyone in a group booking has their own. Changes made in Bookly\'s calendar, its appointments list or the links in its emails reach the pass within seconds; anything else within 15 minutes.', 'walletpassbuilder' ) . ' ' . $updates, ), 'events_manager' => array( 'tagline' => __( 'A pass for every ticket', 'walletpassbuilder' ), 'slug' => 'events-manager', 'auto' => array( __( 'Booking confirmation', 'walletpassbuilder' ), __( '"My Bookings" page', 'walletpassbuilder' ) ), 'email' => array( 'tag' => '#_WALLETPASSBUTTONS', 'link' => '#_WALLETPASSLINK', 'where' => __( 'Events › Settings › Emails', 'walletpassbuilder' ), ), 'how' => __( 'Every space in a booking gets its own pass, so a booking for three is three tickets to share and scan. A pass is issued once a booking is pending approval or approved; unpaid and waitlisted bookings get theirs when they\'re approved.', 'walletpassbuilder' ) . ' ' . $updates, ), 'eventon' => array( 'tagline' => __( 'A pass for every event', 'walletpassbuilder' ), 'slug' => 'eventon-lite', 'auto' => $evo_box ? array() : array( __( 'Event card', 'walletpassbuilder' ) ), 'todo' => $evo_box ? __( 'Event card: add the "Add to Wallet" box in EventON › Settings › EventCard (your event card has its own layout).', 'walletpassbuilder' ) : '', 'email' => null, 'how' => __( 'EventON events have no tickets or attendees, so each upcoming event gets one pass that everyone shares, added from the event card. It uses one pass credit per event, however many people add it. When the event moves, is cancelled or postponed, or a repeating event\'s next date comes round, the pass updates on every phone that saved it.', 'walletpassbuilder' ), 'idle' => __( 'Turns on in one click. Each upcoming event then gets its pass, using one credit per event.', 'walletpassbuilder' ), /* translators: %s: the events plugin, EventON. */ 'on' => __( '%s is on. Upcoming events are getting their passes.', 'walletpassbuilder' ), /* translators: %s: the events plugin, EventON. */ 'off' => __( '%s is off. New events no longer get a pass; passes already issued keep working.', 'walletpassbuilder' ), 'banner' => __( 'Every upcoming event gets its own card on this pass, shared by everyone who adds it.', 'walletpassbuilder' ), 'save_on' => __( 'Saving updates every event pass, including ones already on phones. Each new event uses one pass credit.', 'walletpassbuilder' ), /* translators: %s: the events plugin, EventON. */ 'save_off' => __( 'Saved for when you turn %s on. Each upcoming event then uses one pass credit.', 'walletpassbuilder' ), 'save_button' => __( 'Save event pass', 'walletpassbuilder' ), 'confirm' => __( 'Save the event pass? Event passes already on people\'s phones update to it.', 'walletpassbuilder' ), ), 'event_espresso' => array( 'tagline' => __( 'A pass for every registration', 'walletpassbuilder' ), 'slug' => 'event-espresso-decaf', 'auto' => array( __( 'Thank-you page', 'walletpassbuilder' ) ), 'email' => array( 'tag' => '[WALLET_PASS_BUTTONS]', 'link' => '[WALLET_PASS_LINK]', 'where' => __( 'Event Espresso › Messages, in a registration or ticket message (or inside [ATTENDEE_LIST] for every ticket)', 'walletpassbuilder' ), ), 'how' => __( 'Every registration, which Event Espresso makes for each ticket in an order, gets its own pass with the attendee, ticket type and registration code as a QR code. A pass is issued once a registration is approved or awaiting review; unpaid and waitlisted ones get theirs when they\'re approved. Changes to the event, its dates, tickets, venue or the attendee\'s details reach the pass.', 'walletpassbuilder' ) . ' ' . $updates, ), 'wp_event_manager' => array( 'tagline' => __( 'A pass for every ticket and registration', 'walletpassbuilder' ), 'slug' => 'wp-event-manager', 'auto' => array( __( 'Order received page', 'walletpassbuilder' ), __( 'My Account orders', 'walletpassbuilder' ), __( 'Order emails', 'walletpassbuilder' ) ), 'email' => array( 'tag' => '[wallet_pass_buttons]', 'link' => '[wallet_pass_link]', 'where' => __( 'Registrations › Mail Notifications (and Sell Tickets\' ticket email)', 'walletpassbuilder' ), ), 'how' => __( 'Every registration gets its own pass, and with Sell Tickets every ticket bought is a registration for its attendee, so an order for three is three passes. The QR code is WP Event Manager\'s own ticket code, so its check-in app scans the pass, and check-ins show on it. A pass is issued once a registration is new or confirmed and its order is paid; waitlisted ones get theirs when they\'re confirmed. Needs the Registrations add-on.', 'walletpassbuilder' ) . ' ' . $updates, ), 'woocommerce' => array( 'tagline' => __( 'A pass for every item you choose', 'walletpassbuilder' ), 'slug' => 'woocommerce', 'auto' => $woo_none ? array() : array( __( 'Order received page', 'walletpassbuilder' ), __( 'My Account orders', 'walletpassbuilder' ), __( 'Order emails', 'walletpassbuilder' ) ), 'todo' => $woo_none ? __( 'Products: tick "Wallet pass" on the products that should issue one (Products › Edit › Product data › General).', 'walletpassbuilder' ) : '', 'email' => null, 'how' => __( 'Tick "Wallet pass" on a product and each unit bought gets its own pass once the order is paid: three tickets are three passes, each with its own code to scan. A cancelled, refunded or failed order, or a refunded unit, shows as cancelled on the pass. Each pass uses one pass credit.', 'walletpassbuilder' ), 'idle' => __( 'Turns on in one click. Nothing is issued until you tick "Wallet pass" on a product.', 'walletpassbuilder' ), ), ); return isset( $info[ $host ] ) ? $info[ $host ] : null; } /** * The on/off switch for an integration. * * @param string $host Host plugin key. * @param string $label Host plugin name. * @param bool $on Whether it's on. */ private static function integration_switch( $host, $label, $on ) { /* translators: %s: the booking plugin, such as LatePoint. */ $aria = sprintf( $on ? __( 'Turn off for %s', 'walletpassbuilder' ) : __( 'Turn on for %s', 'walletpassbuilder' ), $label ); echo '
    '; wp_nonce_field( 'walletpb_integration' ); echo ''; echo ''; echo '
    '; } /** * Integrations tab: a compact card per installed booking plugin, the others as one line. */ private static function tab_integrations() { if ( ! self::require_connection() ) { return; } $installed = array(); $missing = array(); foreach ( WalletPB_Integration::hosts() as $host => $entry ) { $first = $entry[1][0]; if ( $first::available() ) { $installed[ $host ] = $entry; } else { $missing[ $host ] = $entry; } } // Those that are on first. uksort( $installed, function ( $x, $y ) use ( $installed ) { $on_x = $installed[ $x ][1][0]::enabled(); $on_y = $installed[ $y ][1][0]::enabled(); return $on_x === $on_y ? 0 : ( $on_x ? -1 : 1 ); } ); // None of the supported plugins is active: say what this tab does and offer them. if ( ! $installed ) { echo '

    ' . esc_html__( 'Give every booking or ticket its own wallet pass', 'walletpassbuilder' ) . '

    '; echo '

    ' . esc_html__( 'Install one of these plugins and turn it on here. Each customer then gets their own pass, kept up to date when their booking changes.', 'walletpassbuilder' ) . '

    ' . esc_html__( 'Not using a booking plugin? The Generic Pass and the Add to Wallet button work on any page.', 'walletpassbuilder' ) . ' ' . esc_html__( 'Add to Wallet button', 'walletpassbuilder' ) . '

    '; echo '

    ' . esc_html__( 'Use a different booking or events plugin?', 'walletpassbuilder' ) . ' ' . '' . esc_html__( 'Request an integration', 'walletpassbuilder' ) . '' . '

    '; return; } $credits = WalletPB_API::credits(); echo '
    ' . esc_html__( 'Every pass a booking or ticket gets uses one pass credit.', 'walletpassbuilder' ) . ''; if ( ! is_wp_error( $credits ) && isset( $credits['remaining'] ) ) { /* translators: %s: number of credits. */ echo '' . esc_html( sprintf( _n( '%s credit left', '%s credits left', (int) $credits['remaining'], 'walletpassbuilder' ), number_format_i18n( (int) $credits['remaining'] ) ) ) . ''; } echo '
    '; echo '
    '; foreach ( $installed as $host => $entry ) { list( $label, $classes ) = $entry; $info = self::integration_info( $host ); $on = $classes[0]::enabled(); // Inside the card the plugin's name goes without saying: "Amelia events" is "Events". $kind = function ( $class ) use ( $label ) { $short = trim( str_replace( $label, '', $class::template_label() ) ); return '' !== $short ? $short : $class::template_label(); }; echo '
    '; echo '

    ' . esc_html( $label ) . '

    ' . esc_html( $info['tagline'] ) . '

    '; self::integration_switch( $host, $label, $on ); echo '
    '; if ( $on ) { $stats = array(); foreach ( $classes as $class ) { $count = WalletPB_Cards::count( $class::SOURCE ); /* translators: %s: number of passes. */ $text = sprintf( _n( '%s pass issued', '%s passes issued', $count, 'walletpassbuilder' ), number_format_i18n( $count ) ); if ( count( $classes ) > 1 ) { /* translators: 1: kind of booking, such as "Events", 2: "3 passes issued". */ $text = sprintf( __( '%1$s: %2$s', 'walletpassbuilder' ), ucfirst( $kind( $class ) ), $text ); } $settings = $class::settings(); $stats[] = esc_html( $text ) . ( '' !== $settings['pass_url'] ? ' ' . esc_html__( 'Open pass', 'walletpassbuilder' ) . ' ↗' : '' ); } echo '

    ' . implode( '', $stats ) . '

    '; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped above. echo '

    ' . esc_html__( 'Where customers get their pass', 'walletpassbuilder' ) . '

      '; foreach ( $info['auto'] as $place ) { echo '
    • ' . esc_html( $place ) . ' ' . esc_html__( 'automatic', 'walletpassbuilder' ) . '
    • '; } if ( ! empty( $info['todo'] ) ) { echo '
    • ' . esc_html( $info['todo'] ) . '
    • '; } if ( $info['email'] ) { echo '
    • ' . esc_html__( 'Emails: add', 'walletpassbuilder' ) . ' ' . esc_html( $info['email']['tag'] ) . ' '; /* translators: 1: where the emails are edited, 2: the link-only tag. */ echo '' . esc_html( sprintf( __( 'In %1$s. For just the link, use %2$s.', 'walletpassbuilder' ), $info['email']['where'], $info['email']['link'] ) ) . '
    • '; } echo '
    '; foreach ( $classes as $class ) { /* translators: %s: the template, such as "Amelia events". */ $edit = 1 === count( $classes ) ? __( 'Edit design', 'walletpassbuilder' ) : sprintf( __( 'Edit %s', 'walletpassbuilder' ), $kind( $class ) ); echo '' . esc_html( $edit ) . ''; } echo '
    '; } else { echo '

    ' . esc_html( ! empty( $info['idle'] ) ? $info['idle'] : __( 'Turns on in one click. No credits are used until a booking comes in.', 'walletpassbuilder' ) ) . '

    '; } echo '
    ' . esc_html__( 'How it works', 'walletpassbuilder' ) . '

    ' . esc_html( $info['how'] ) . '

    '; echo '
    '; } echo '
    '; if ( $missing ) { $links = array(); foreach ( $missing as $host => $entry ) { $info = self::integration_info( $host ); $text = esc_html( $entry[0] ); $links[] = current_user_can( 'install_plugins' ) && $info ? $text . ' ' . esc_html__( 'Install', 'walletpassbuilder' ) . '' : $text; } echo '

    ' . esc_html__( 'Also works with:', 'walletpassbuilder' ) . ' ' . implode( '', $links ) . '

    '; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped above. } echo '

    ' . esc_html__( 'Use a different booking or events plugin?', 'walletpassbuilder' ) . ' ' . '' . esc_html__( 'Request an integration', 'walletpassbuilder' ) . '' . '

    '; } /** * Activity tab. */ private static function tab_activity() { $per_page = 50; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- only picks the page of results. $paged = isset( $_GET['paged'] ) ? max( 1, absint( $_GET['paged'] ) ) : 1; $rows = WalletPB_Activity::recent( $per_page, ( $paged - 1 ) * $per_page ); $total = WalletPB_Activity::count(); echo '
    '; if ( ! $rows ) { echo '

    ' . esc_html__( 'Nothing yet. Every pass the plugin creates or updates is listed here, with the reason if it failed.', 'walletpassbuilder' ) . '

    '; return; } $labels = array( 'create_pass' => __( 'Create pass', 'walletpassbuilder' ), 'update_pass' => __( 'Update pass', 'walletpassbuilder' ), 'issue_card' => __( 'Issue card', 'walletpassbuilder' ), 'update_card' => __( 'Update card', 'walletpassbuilder' ), ); $status = array( 'success' => __( 'Done', 'walletpassbuilder' ), 'failed' => __( 'Failed', 'walletpassbuilder' ), 'retried' => __( 'Failed, then retried', 'walletpassbuilder' ), ); echo ''; foreach ( array( __( 'Date', 'walletpassbuilder' ), __( 'Action', 'walletpassbuilder' ), __( 'Source', 'walletpassbuilder' ), __( 'Customer', 'walletpassbuilder' ), __( 'Status', 'walletpassbuilder' ), __( 'Details', 'walletpassbuilder' ), __( 'Pass', 'walletpassbuilder' ), '' ) as $heading ) { echo ''; } echo ''; foreach ( $rows as $row ) { echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; } echo '
    ' . esc_html( $heading ) . '
    ' . esc_html( get_date_from_gmt( $row['created_at'], get_option( 'date_format' ) . ' ' . get_option( 'time_format' ) ) ) . '' . esc_html( isset( $labels[ $row['action'] ] ) ? $labels[ $row['action'] ] : $row['action'] ) . '' . esc_html( $row['source'] ) . '' . esc_html( '' !== $row['customer'] ? $row['customer'] : '-' ) . '' . esc_html( isset( $status[ $row['status'] ] ) ? $status[ $row['status'] ] : $row['status'] ) . '' . esc_html( $row['message'] ) . ''; if ( '' !== $row['pass_id'] ) { // Rows from before links were stored look the link up (cached). $link = '' !== $row['pass_url'] ? $row['pass_url'] : WalletPB_Button::pass_url( $row['pass_id'] ); echo '' !== $link ? '' . esc_html( $row['pass_id'] ) . ' ' . esc_html__( 'Open', 'walletpassbuilder' ) . ' ↗' : '' . esc_html( $row['pass_id'] ) . ''; } else { echo '-'; } echo ''; if ( 'failed' === $row['status'] && ! empty( $row['retryable'] ) ) { echo '
    '; wp_nonce_field( 'walletpb_retry' ); echo ''; submit_button( __( 'Retry', 'walletpassbuilder' ), 'small', 'submit', false ); echo '
    '; } echo '
    '; $pages = (int) ceil( $total / $per_page ); if ( $pages > 1 ) { echo '
    '; echo wp_kses_post( paginate_links( array( 'base' => esc_url_raw( add_query_arg( 'paged', '%#%', self::url( 'activity' ) ) ), 'format' => '', 'current' => $paged, 'total' => $pages, ) ) ); echo '
    '; } echo ''; } /** * The Help tab: quick answers, and a message to the support team. */ private static function tab_help() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- only picks what the form starts with. $topic = isset( $_GET['topic'] ) && 'integration' === sanitize_key( wp_unslash( $_GET['topic'] ) ) ? 'integration' : ''; $draft = get_transient( 'walletpb_support_draft_' . get_current_user_id() ); $d = wp_parse_args( is_array( $draft ) ? $draft : array(), array( 'email' => wp_get_current_user()->user_email, 'subject' => 'integration' === $topic ? __( 'Integration request', 'walletpassbuilder' ) : '', 'message' => '', 'details' => true, ) ); if ( 'integration' === $topic && '' === trim( (string) $d['subject'] ) ) { $d['subject'] = __( 'Integration request', 'walletpassbuilder' ); } $mail = '' . esc_html( WalletPB_Support::EMAIL ) . ''; $forum = '' . esc_html__( 'WordPress.org forum', 'walletpassbuilder' ) . ''; echo '
    '; echo '

    ' . esc_html__( 'Quick answers', 'walletpassbuilder' ) . '

    '; $answers = array( __( 'A booking didn\'t get a pass', 'walletpassbuilder' ) => __( 'Check that its integration is on under Integrations, then look in Activity: a failed entry says why, and Retry sends it again once that\'s fixed.', 'walletpassbuilder' ), __( 'Passes on phones haven\'t changed', 'walletpassbuilder' ) => __( 'Saving a template updates the passes already on phones. It can take a few minutes for them to change.', 'walletpassbuilder' ), __( 'When do reminders appear?', 'walletpassbuilder' ) => __( 'iPhone shows the pass on the lock screen at the start time; Google Wallet sends a reminder the day before.', 'walletpassbuilder' ), __( 'What uses pass credits?', 'walletpassbuilder' ) => __( 'Creating a pass uses one credit, and so does each booking\'s own pass. Your plan and balance are on the Settings tab.', 'walletpassbuilder' ), ); foreach ( $answers as $question => $answer ) { echo '
    ' . esc_html( $question ) . '

    ' . esc_html( $answer ) . '

    '; } /* translators: 1: the WordPress.org forum link, 2: the support email address. */ echo '

    ' . wp_kses_post( sprintf( __( 'You can also ask on the %1$s or email %2$s.', 'walletpassbuilder' ), $forum, $mail ) ) . '

    '; echo '
    '; echo '

    ' . esc_html__( 'Message us', 'walletpassbuilder' ) . '

    '; if ( ! WalletPB_API::is_connected() ) { /* translators: %s: the support email address. */ echo '

    ' . wp_kses_post( sprintf( __( 'Connect your account on the Pass Template tab to message us from here, or email %s.', 'walletpassbuilder' ), $mail ) ) . '

    '; return; } echo '

    ' . esc_html__( 'It goes straight to the WalletPassBuilder team, with your account, and we reply by email.', 'walletpassbuilder' ) . '

    '; echo '
    '; wp_nonce_field( 'walletpb_support' ); echo ''; echo ''; echo '

    '; echo '

    '; echo '

    '; echo '

    '; /* translators: %d: how many screenshots a message can carry. */ echo '' . esc_html( sprintf( __( 'Optional. Up to %d images, 5 MB each.', 'walletpassbuilder' ), WalletPB_Support::MAX_SCREENSHOTS ) ) . '

    '; echo '

    '; echo '
    ' . esc_html__( 'What\'s included', 'walletpassbuilder' ) . '
    '; foreach ( WalletPB_Support::diagnostics( $topic ) as $label => $text ) { echo '
    ' . esc_html( $label ) . '
    ' . nl2br( esc_html( $text ) ) . '
    '; } echo '

    ' . esc_html__( 'No customer details are included.', 'walletpassbuilder' ) . '

    '; echo '

    '; echo '
    '; } /** * Settings tab. */ private static function tab_settings() { self::render_account(); echo '

    ' . esc_html__( 'Data', 'walletpassbuilder' ) . '

    '; echo '
    '; wp_nonce_field( 'walletpb_save_settings' ); echo ''; echo '

    '; echo '

    ' . esc_html__( 'Passes in your WalletPassBuilder account are never deleted by this plugin.', 'walletpassbuilder' ) . '

    '; submit_button( __( 'Save settings', 'walletpassbuilder' ) ); echo '
    '; } }