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 '
';
}
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();
?>
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 );
}
}