settings = $settings; $this->security = $security instanceof Security ? $security : new Security( $settings ); } public function init() { add_shortcode( 'careback', array( $this, 'render' ) ); } /** * The number of units above which a line of the form gets a number field: * UNIT_THRESHOLD, or what `careback_unit_threshold` makes it, at least 1. * * @return int */ public static function unit_threshold() { /** * Filters the number of units on one order line above which the form * shows a number field instead of one checkbox per unit. See * api/customer.md. * * @param int $threshold Units; 5 by default. */ $threshold = apply_filters( self::UNIT_THRESHOLD_FILTER, self::UNIT_THRESHOLD ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound -- careback_unit_threshold, the final prefix. return is_numeric( $threshold ) && (int) $threshold >= 1 ? (int) $threshold : self::UNIT_THRESHOLD; } /** Kept for back-compat; delegates to the editable reason list. */ public function reasons() { return Reasons::all( $this->settings ); } public function render() { if ( empty( $this->settings->get( 'portal_enabled' ) ) || ! Plugin::woocommerce_active() ) { return ''; } // A site whose tables are not set up yet takes no request: it says so, // with the way to reach the shop that every customer page carries, and // loads no script that would ask. if ( ! Database::ready() ) { wp_enqueue_style( 'careback', CAREBACK_URL . 'assets/css/portal.css', array(), CAREBACK_VERSION ); return '

' . esc_html( Rest::not_ready_text() ) . '

' . '

' . wp_kses_post( self::contact( $this->settings ) ) . '

'; } wp_enqueue_style( 'careback', CAREBACK_URL . 'assets/css/portal.css', array(), CAREBACK_VERSION ); wp_enqueue_script( 'careback', CAREBACK_URL . 'assets/js/portal.js', array(), CAREBACK_VERSION, true ); // An add-on's bot check: its markup for step one, and what it enqueues. $bot_check = $this->security->bot_check_markup( 'portal' ); $t = Texts::all( $this->settings ); // `reasons` carries the labels; `reasonOrder` carries the order the // dropdown is built in. $reasons = Reasons::all( $this->settings ); // While the statutory withdrawal is offered, the texts that name it; once // a shop has switched it off, those that speak of a return only. $withdrawal = Routes::offered( 'withdrawal', $this->settings ); $text = static function ( $key, $fallback ) use ( $t ) { return isset( $t[ $key ] ) ? $t[ $key ] : $t[ $fallback ]; }; // The shop's returns policy page, while published (N-13): linked under the // question of step 2 — except on that page itself — and in the consent. $policy = Returns_Policy::link( $this->settings ); if ( null !== $policy ) { $policy['showLink'] = (int) get_queried_object_id() !== (int) $policy['id']; } wp_localize_script( 'careback', 'CarebackData', array( 'lookupUrl' => rest_url( Rest::NS . '/lookup' ), 'submitUrl' => rest_url( Rest::NS . '/submit' ), 'reasons' => $reasons, // Which of them each route offers, and whether the route asks at // all. The full map above stays: a browser that has an older script // cached keeps rendering the flat list and nothing breaks. 'reasonRoutes' => Reasons::by_route( $this->settings ), 'reasonModes' => array_combine( Routes::keys(), array_map( function ( $route ) { return Reasons::mode( $route, $this->settings ); }, Routes::keys() ) ), // The routes in display order, each with its own texts and the // facts the script decides on (Routes::for_portal()). 'routes' => Routes::for_portal( $this->settings ), 'reasonOrder' => array_map( 'strval', array_keys( $reasons ) ), 'fields' => Fields::flags( $this->settings ), 'unitThreshold' => self::unit_threshold(), // Still handed over, so a browser that cannot reach the route // below has something to send. Its age is why the route exists. 'timeToken' => $this->security->time_token(), 'tokenUrl' => rest_url( Rest::NS . '/token' ), 'dispatchUrl' => rest_url( Rest::NS . '/dispatch-proof' ), // N-13 and N-12: the page, and whether the voluntary routes ask for // the consent with it. 'policy' => $policy, 'consent' => Returns_Policy::consent_on( $this->settings ), 'i18n' => array( 'dispatchPrompt' => $t['dispatch_prompt'], 'dispatchHint' => $t['dispatch_hint'], 'dispatchButton' => $t['dispatch_button'], 'dispatchThanks' => $t['dispatch_thanks'], 'checking' => $t['checking'], 'needOrder' => $t['need_order'], 'needEmail' => $t['need_email'], 'noOrderItems' => $t['no_order_items'], 'quantity' => $t['quantity'], 'selectItems' => $t['select_items'], 'unitClaimed' => $t['unit_claimed'], 'unitClaimedWithdrawal' => $withdrawal ? $t['unit_claimed_withdrawal'] : '', 'lineFullyClaimed' => $t['line_fully_claimed'], 'unitsRightsNotice' => $withdrawal ? $t['units_rights_notice'] : $text( 'units_rights_notice_return', 'units_rights_notice' ), 'unitsAvailable' => $t['units_available'], 'reason' => $t['reason'], 'choose' => $t['choose'], 'comment' => $t['comment'], 'send' => $t['send'], 'back' => $t['back'], 'sending' => $t['sending'], 'thanks' => $t['thanks'], 'error' => $t['error'], 'noItems' => $t['no_items'], 'needReason' => $t['need_reason'], 'needComment' => $t['need_comment'], 'flowQuestion' => $t['flow_question'], 'flowNoneAvailable' => $t['flow_none_available'], // The three steps, the check step and the done screen (N-04). 'continue' => $t['continue'], 'stepFind' => $t['step_find'], 'stepChoose' => $t['step_choose'], 'stepCheck' => $t['step_check'], 'progressShort' => $t['progress_short'], 'orderLine' => $t['order_line'], 'reviewTitle' => $t['review_title'], 'reviewNotSent' => $t['review_not_sent'], 'reviewRequest' => $t['review_request'], 'reviewOrder' => $t['review_order'], 'reviewOrderValue' => $t['review_order_value'], 'reviewItems' => $t['review_items'], 'reviewName' => $t['review_name'], 'reviewReceiptTo' => $t['review_receipt_to'], 'reviewPolicy' => $t['review_policy'], 'reviewPolicyAccepted' => $t['review_policy_accepted'], 'reviewCost' => $t['review_cost'], 'reviewUnits' => $t['review_units'], 'reviewReceiptNote' => $t['review_receipt_note'], 'reviewNameInput' => $t['review_name_input'], 'reviewNeedName' => $t['review_need_name'], 'errConfirmNetwork' => $t['err_confirm_network'], 'errSessionExpired' => $t['err_session_expired'], 'policyLink' => $t['policy_link'], 'opensNewTab' => $t['opens_new_tab'], 'consentLabel' => $t['consent_label'], 'consentLink' => $t['consent_link'], 'needConsent' => $t['need_consent'], ), ) ); /** * Fires once the portal's script and stylesheet are enqueued and its data * handed over, on a page that renders the portal. * * The place to enqueue a script that extends the portal: give it the * portal's handle as a dependency and it runs after `window.careback` * exists. * * @param string $handle Handle of the portal's script and stylesheet. * @param Settings $settings Plugin settings. */ do_action( self::ASSETS_ACTION, 'careback', $this->settings ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound -- careback_portal_assets, the final prefix. // The fallback route must always be visible: a customer must be able to // declare a return or withdrawal without using this form. $fallback = self::contact( $this->settings ); // The model withdrawal form with the shop's details (N-114), on a page of // its own, while statutory withdrawal is on. $model_form = Model_Form::offered( $this->settings ) ? Model_Form::url() : ''; // The steps of the form above it (N-04): a list naming each, the current // one marked; on a phone a line instead. $steps = array( $t['step_find'], $t['step_choose'], $t['step_check'] ); ob_start(); ?>
    $name ) : ?> >

settings, array( 1, count( $steps ), $steps[0] ) ) ); ?>

get( 'fallback_contact', '' ) ); if ( '' === $contact ) { $contact = Texts::format( 'fallback_default', $settings, array( get_option( 'admin_email' ) ) ); } return $contact; } /** * The same contact line as plain text, for answers the portal shows as text * — such as the one sending a customer to the shop for an order the form * cannot take — so they read the same contact there as below the form. * * A link keeps where it leads: "Write to us" pointing at an address reads * "Write to us (care@shop.example)", since the text alone cannot be clicked. * * @param Settings $settings Plugin settings. * @return string */ public static function contact_text( Settings $settings ) { $html = preg_replace_callback( '#]*?href\s*=\s*(["\'])(.*?)\1[^>]*>(.*?)#is', static function ( $link ) { $label = trim( html_entity_decode( wp_strip_all_tags( $link[3] ), ENT_QUOTES, 'UTF-8' ) ); $target = html_entity_decode( $link[2], ENT_QUOTES, 'UTF-8' ); // An address, not the scheme or the subject line the link carries. if ( preg_match( '#^(mailto|tel):([^?]*)#i', $target, $parts ) ) { $target = rawurldecode( $parts[2] ); } $target = trim( $target ); // A link to a page of the shop itself, written as a path. if ( 0 === strpos( $target, '/' ) && 0 !== strpos( $target, '//' ) ) { $target = home_url( $target ); } if ( '' === $target || false !== stripos( $label, $target ) ) { return esc_html( $label ); } return esc_html( '' === $label ? $target : $label . ' (' . $target . ')' ); }, wp_kses_post( self::contact( $settings ) ) ); if ( null === $html ) { $html = wp_kses_post( self::contact( $settings ) ); } $text = html_entity_decode( wp_strip_all_tags( $html ), ENT_QUOTES, 'UTF-8' ); $squeezed = preg_replace( '/\s+/u', ' ', $text ); return trim( null === $squeezed ? $text : $squeezed ); } }