Información de medios de pago y cuotas

En este tutorial, vemos cómo agregar visibilidad sobre medios de pago y cuotas en tu diseño:

El código en este tutorial incluye:

  • Cuotas en el listado de productos, el detalle del producto y el carrito 
  • Modal que muestra el detalle de los medios de pago ofrecidos por la tienda

HTML

1. Lo primero que vamos a hacer es llamar al componente de installments para mostrar las cuotas en el listado de los productos.

En el archivo item.tpl vamos a sumar el siguiente llamado

{{ component('installments', {'location' : 'product_item', container_classes: { installment: "item-installments"}}) }} 

Este componte incluye el mensaje de cuotas y permite los siguientes parámetros:

location:

  • cart: Lo vamos a usar al incluirlo en el carrito
  • product_item: Lo vamos a usar al incluirlo en el item del listado de productos
  • product_detail: Lo vamos a usar al incluirlo en el detalle del producto, por ejemplo debajo del precio

short_wording: Si es true, sólo en español se mostrarán las cuotas como "12x $200 sin interés" vs "12 cuotas sin interés de $200"

container_classes.installment: Usado para pasarle clases al contenedor general del mensaje de cuotas

Debería quedar algo similar a esto:

 2De la misma manera que lo hicimos para el item del listado de productos, ahora vamos a hacerlo para el mensaje de cuotas en el carrito de compras. Vamos a buscar el tpl cart-totals.tpl o donde tengamos el total del carrito, y justo debajo vamos a sumar el siguiente código

{{ component('installments', {'location': 'cart', container_classes: { installment: "mt-1 font-weight-bold text-right"}}) }}

Recordemos los parámetros que usamos para el item de producto, pero en este caso usamos "location" : "cart" y las clases que necesitemos para el CSS, dejando algo como el siguiente ejemplo:

3.  Dejamos el detalle del producto para el final, ya que vamos a tener 2 partes: el mensaje de cuotas debajo del precio y el popup que muestra el detalle de los medios de pago.

Lo primero que vamos a hacer es buscar donde tenemos el formulario de producto, por ejemplo puede ser el tpl product-form.tpl y debajo del precio incluir el componente de installments como lo hicimos anteriormente (no olvidemos de poner el parámetro "location" correctamente)

{{ component('installments', {'location' : 'product_detail', container_classes: { installment: "product-detail-installments text-center text-md-left mb-2" }}) }}

Debería quedar algo similar a esto:

Por último para el popup de medios de pago agregamos el archivo product-payment-details.tpl dentro de la carpeta con el nombre product, con el siguiente código.

{# Product payments details #}

{% if product.installments_info_from_any_variant %}

    {% embed "snipplets/modal.tpl" with{
        modal_id: 'installments-modal', 
        modal_position: 'bottom', 
        modal_transition: 'slide', 
        modal_header: true, 
        modal_footer: true, 
        modal_width: 'centered', 
        modal_mobile_full_screen: 'true'} %}
        {% block modal_head %}
            {{ 'Medios de pago' | translate }}
        {% endblock %}
        {% block modal_body %}

            {# Modal header and gateways tab links #}

            {{ component('payments/payments-details',
                {
                    text_classes: {
                        text_accent: "label label-accent ml-1",
                        subtitles: "mb-3",
                        text_big: "font-big",
                        text_small: "font-small",
                        align_right: "text-right"
                    },
                    spacing_classes: {
                        top_1x: "mt-1",
                        top_2x: "mt-2",
                        top_3x: "mt-3",
                        right_1x: "mr-1",
                        right_2x: "mr-2",
                        right_3x: "mr-3",
                        bottom_1x: "mb-1",
                        bottom_2x: "mb-2",
                        bottom_3x: "mb-3",
                        left_3x: "ml-3",
                    },
                    container_classes : {
                        payment_method: "card p-3"
                    }
                })
            }}
        {% endblock %}
        {% block modal_foot %}
            <div class="text-right">
                <span class="js-modal-close js-fullscreen-modal-close btn-link pull-right">{{ 'Volver al producto' | translate }}</span>
            </div>
        {% endblock %}
    {% endembed %}
{% endif %}

Los parámetros que acepta el componente de payments-details son principalmente clases para CSS

Clases de textos:

  • text_classes.text_accent: Usado para las partes de acento como los descuentos
  • text_classes.subtitles: Usado para los subtítulos dentro del popup
  • text_classes.text_big: Usado para los textos grandes que siguen siendo más chicos que un título
  • text_classes.text_small: Usado para textos pequeños

Clases de espaciado:

  • spacing_classes.top_1x: Usado para los márgenes hacia arriba en 1x
  • spacing_classes.right_1x: Usado para los márgenes a la derecha en 1x
  • spacing_classes.right_2x: Usado para los márgenes a la derecha en 2x
  • spacing_classes.right_3x: Usado para los márgenes a la derecha en 3x
  • spacing_classes.bottom_1x: Usado para los márgenes hacia abajo en 1x
  • spacing_classes.bottom_2x: Usado para los márgenes hacia abajo en 2x
  • spacing_classes.bottom_3x: Usado para los márgenes hacia abajo en 3x
  • spacing_classes.left_3x: Usado para los márgenes a la izquierda en 3x

Y lo incluimos al final del formulario de producto en el archivo product-form.tpl

{# Product payments details #}

{% include 'snipplets/product/product-payment-details.tpl' %}

Este popup incluye los detalles de todos los medios de pago así como los descuentos que pueda ofrecer cada uno.

Antes de avanzar no tenemos que olvidarnos de sumar el link que abre el popup. Podemos agregarlo donde necesitemos:

<a id="btn-installments" class="btn-link" {% if not (product.get_max_installments and product.get_max_installments(false)) %}style="display: none;"{% endif %}>
    {{ "Ver medios de pago" | translate }}
</a>

4. Vamos a necesitar agregar algunos IDs y clases en el detalle del producto para que las cuotas se actualicen al cambiar de variante. En el theme Base modificamos el template product.tpl y el snipplet product-form.tpl; pero en tu caso puede que solo necesites modificar product.tpl.

product.tpl

En el div padre que engloba a todo el contenido del detalle de producto agregar los siguientes IDs y selectores “js-...”

<div id="single-product" class=”js-product-detail js-product-container" data-variants="{{product.variants_object | json_encode }}" itemscope itemtype="http://schema.org/Product">

product-form.tpl

En este archivo vamos a reemplazar el código que tengas en relación al precio del producto por lo siguiente (recordá que este cambio aplica en este snipplet pero en tu caso podés aplicarlo donde sea que tengas el precio del producto)

{# Product price #}


<div class="price-container text-center text-sm-left" itemprop="offers" itemscope itemtype="http://schema.org/Offer">
    <span class="d-inline-block">
       <h4 id="compare_price_display" class="js-compare-price-display price-compare {% if product_can_show_installments or (product.promotional_offer and not product.promotional_offer.script.is_percentage_off) %}mb-2{% endif %}" {% if not product.compare_at_price or not product.display_price %}style="display:none;"{% else %} style="display:block;"{% endif %}>{% if product.compare_at_price and product.display_price %}{{ product.compare_at_price | money }}{% endif %}</h4>
    </span>
    <spa class="d-inline-block">
        <h4 class="js-price-display {% if product_can_show_installments or (product.promotional_offer and not product.promotional_offer.script.is_percentage_off) %}mb-2{% endif %}" id="price_display" itemprop="price"{% if product.display_price %} content="{{ product.price / 100 }}"{% endif %} {% if not product.display_price %}style="display:none;"{% endif %}>{% if product.display_price %}{{ product.price | money }}{% endif %}</h4>
    </span>
    <meta itemprop="priceCurrency" content="{{ product.currency }}" />
    {% if product.stock_control %}
        <meta itemprop="inventoryLevel" content="{{ product.stock }}" />
        <meta itemprop="availability" href="http://schema.org/{{ product.stock ? 'InStock' : 'OutOfStock' }}" content="{{ product.stock ? 'In stock' : 'Out of stock' }}" />
    {% endif %}
</div>

También necesitamos agregar la clase js-addtocart al botón de “Agregar al carrito” quedando así:

{% set state = store.is_catalog ? 'catalog' : (product.available ? product.display_price ? 'cart' : 'contact' : 'nostock') %}
{% set texts = {'cart': "Agregar al carrito", 'contact': "Consultar precio", 'nostock': "Sin stock", 'catalog': "Consultar"} %}
<input type="submit" class="js-addtocart js-prod-submit-form btn btn-primary btn-block mb-4 {{ state }}" value="{{ texts[state] | translate }}" {% if state == 'nostock' %}disabled{% endif %} />

5. Ahora necesitamos crear el snipplet para el componente modal o popup dentro de la carpeta snipplets. Este tpl se llama modal.tpl y el código es:

{# /*============================================================================
  #Modal
==============================================================================*/

#Properties
    // ID
    // Position - Top, Right, Bottom, Left
    // Transition - Slide and Fade
    // Width - Full and Box
    // modal_form_action - For modals that has a form


#Head
    // Block - modal_head
#Body
    // Block - modal_body
#Footer
    // Block - modal_footer

#}


{% set modal_overlay = modal_overlay | default(true) %}


<div id="{{ modal_id }}" class="js-modal modal modal-{{ modal_class }} modal-{{modal_position}} transition-{{modal_transition}} modal-{{modal_width}} transition-soft" style="display: none;">
    {% if modal_form_action %}
    <form action="{{ modal_form_action }}" method="post" class="{{ modal_form_class }}">
    {% endif %}
    <div class="js-modal-close modal-header">
        <span class="modal-close">
            {% include "snipplets/svg/times.tpl" with {svg_custom_class: "icon-inline svg-icon-text"} %}
        </span>
        {% block modal_head %}{% endblock %}
    </div>
    <div class="modal-body">
        {% block modal_body %}{% endblock %}
    </div>
    {% if modal_footer %}
        <div class="modal-footer d-md-block">
            {% block modal_foot %}{% endblock %}
        </div>
    {% endif %}
    {% if modal_form_action %}
    </form>
    {% endif %}
</div>

6. Agregamos la clase js-variation-option dentro del select para las variantes del detalle del producto. En el theme Base esto está en el tpl product-variants.tpl dentro de la carpeta snipplets/product. En tu diseño puede que este archivo se llame variants.tpl o tengas que aplicar el cambio directamente en el template product.tpl

7. Por último para la parte de HTML, necesitamos agregar una carpeta SVG dentro de la carpeta snipplets. Acá vamos sumar los SVGs para el icono de la tarjeta de crédito en el detalle del producto con el nombre credit-card-blank.tpl, y el icono para cerrar el popup con el nombre times.tpl

credit-card-blank.tpl

<svg class="{{ svg_custom_class }}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M527.9 32H48.1C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48.1 48h479.8c26.6 0 48.1-21.5 48.1-48V80c0-26.5-21.5-48-48.1-48zm-6 400H54.1c-3.3 0-6-2.7-6-6V86c0-3.3 2.7-6 6-6h467.8c3.3 0 6 2.7 6 6v340c0 3.3-2.7 6-6 6zM192 364v8c0 6.6-5.4 12-12 12h-72c-6.6 0-12-5.4-12-12v-8c0-6.6 5.4-12 12-12h72c6.6 0 12 5.4 12 12zm192 0v8c0 6.6-5.4 12-12 12H236c-6.6 0-12-5.4-12-12v-8c0-6.6 5.4-12 12-12h136c6.6 0 12 5.4 12 12z"/></svg>

times.tpl

<svg class="{{ svg_custom_class }}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path d="M207.6 256l107.72-107.72c6.23-6.23 6.23-16.34 0-22.58l-25.03-25.03c-6.23-6.23-16.34-6.23-22.58 0L160 208.4 52.28 100.68c-6.23-6.23-16.34-6.23-22.58 0L4.68 125.7c-6.23 6.23-6.23 16.34 0 22.58L112.4 256 4.68 363.72c-6.23 6.23-6.23 16.34 0 22.58l25.03 25.03c6.23 6.23 16.34 6.23 22.58 0L160 303.6l107.72 107.72c6.23 6.23 16.34 6.23 22.58 0l25.03-25.03c6.23-6.23 6.23-16.34 0-22.58L207.6 256z"/></svg>

CSS

Requisito:

Tener agregados en tu diseño las clases helpers. Podés seguir este este pequeño tutorial para hacerlo (simplemente es copiar y pegar algunas clases, no toma más de 1 minuto).

1.Agregamos el siguiente SASS de colores en style-colors.scss.tpl (o la hoja de tu diseño que tenga los colores y tipografías de la tienda). Recordá que las variables de colores y tipografías pueden variar respecto a tu diseño:

@mixin prefix($property, $value, $prefixes: ()) {
  @each $prefix in $prefixes {
      #{'-' + $prefix + '-' + $property}: $value;
  }
    #{$property}: $value;
}


/* // Wrappers */


.box{
  float: left;
  width: 100%;
  margin-bottom: 20px;
  padding:8px;
  border:1px solid rgba($main-foreground, .2);
}


/* // Dividers */ 


.divider{
  margin-top: 20px;
  margin-bottom: 20px;
  clear: both;
  border-bottom: 1px solid rgba($main-foreground, .1);
}


/* // Modals */


.modal{
  color: $main-foreground;
  background-color:$main-background;
}




/* // Links */


.btn-link{
  color: $primary-color;
  fill: $primary-color;
  text-transform: uppercase;
  border-bottom: 1px solid;
  font-weight: bold;
  cursor: pointer;
  &:hover,
  &:focus{
    color: rgba($primary-color, .5);
    fill: rgba($primary-color, .5);
  }
}


/* // Tables */ 


.table{
  background-color: $main-background;
  color: $main-foreground;
  tbody{
    tr:nth-child(odd){
      background-color: rgba($main-foreground, .05);
    }
  }
  th{
    padding: 8px;
    text-align: left;
  }
}


/* // Tabs */


.tab-group{
  border-bottom: 1px solid rgba($main-foreground, .1);
  .tab{
    &-link{
      color: $main-foreground;
    }
    &.active{
      .tab-link{
        border-bottom: 2px solid rgba($primary-color, .5);
        color: $primary-color;
      }
    }
  }
}

2. Agregar los estilos dentro del archivo static/style-critical.tpl 

Si en tu diseño usas una hoja de estilos para el CSS crítico, vamos a necesitar el siguiente código dentro de la misma, pero si no es el caso entonces podés unificar el CSS de los pasos 2 y 3 en un solo archivo.

/* // Images */


.card-img{
  margin: 0 5px 5px 0;
  border: 1px solid #00000012;
}
.card-img-small{
  height: 25px;
}
.card-img-medium{
  height: 35px;
}
.card-img-big{
  height: 50px;
}

3. Agregar los estilos dentro del archivo static/style-async.tpl 

Si en tu diseño usas una hoja de estilos para CSS asíncrono, vamos a necesitar el siguiente código dentro de la misma, pero si no es el caso entonces podés unificar el CSS de los pasos 2 y 3 en un solo archivo.

@mixin prefix($property, $value, $prefixes: ()) {
  @each $prefix in $prefixes {
      #{'-' + $prefix + '-' + $property}: $value;
  }
    #{$property}: $value;
}


/* // Modals */


.modal {
  position: fixed;
  top: 0;
  display: block;
  width: 80%;
  height: 100%;
  padding: 10px;
  -webkit-overflow-scrolling: touch;
  overflow-y: auto;
  transition: all .2s cubic-bezier(.16,.68,.43,.99);
  z-index: 20000;
  &-header{
    width: calc(100% + 20px);
    margin: -10px 0 10px -10px;
    padding: 10px 15px;
    font-size: 20px;
  }
  &-footer{
    padding: 10px;
    clear: both;
  }
  &-full {
    width: 100%;
  }
  &-docked-sm{
    width: 100%;
  }
  &-docked-small{
    width: 80%;
  }
  &-top{
    top: -100%;
    left: 0;
  }
  &-bottom{
    top: 100%;
    left: 0;
  }
  &-left{
    left: -100%;
  }
  &-right{
    right: -100%;
  }
  &-centered{
    height: 100%;
    width: 100%;
  }
  &-top.modal-show,
  &-bottom.modal-show {
    top: 0;
  }
  &-left.modal-show {
    left: 0;
  }
  &-right.modal-show {
    right: 0;
  }
  &-close { 
    display: inline-block;
    padding: 1px 5px 5px 0;
    margin-right: 5px;
    vertical-align: middle;
    cursor: pointer;
  }
  .tab-group{
    margin:  0 -10px 20px -10px;
  }
}

.modal-overlay{
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: #00000047;
  z-index: 10000;
}


/* // Tables */


.table{
  width: 100%;
  border-collapse: collapse;
  border-spacing: 0;
  thead{
    th{
      padding: 8px;
      &:first-of-type{
        padding-left: 0;
      }
    }
  }
  td{
    padding: 8px;
    text-align: left;
  }
}

/* // Tabs */

.tab-group{
  width: 100vw;
  padding: 0;
  overflow-x: scroll;
  white-space: nowrap;
  .tab{
    display: inline-flex;
    float: none;
    &-link{
      float: left;
      padding: 10px;
      text-align: center;
    }
  }
}


.tab-panel:not(.active){
  display: none;
}
.tab-panel.active{
  display: block;
}




/* // Min width 768px */


@media (min-width: 768px) { 


/* //// Components */


 /* Modals */


  .modal{
    &-centered{
      height: 80%;
      width: 80%;
      left: 10%;
      margin: 5% auto;
    }
    &-docked-sm{
      width: 500px;
    }
    &-docked-small{
      width: 350px;
    }
  }


 /* Tabs */


  .tab-group{
    width: calc(100% + 20px);
    overflow-x: auto;
    white-space: normal;
    .tab{
      float: left;
    }
  }
}

JS

⚠️ A partir del día 30 de enero de 2023, la librería jQuery será removida del código de nuestras tiendas, por lo tanto la función "$" no podrá ser utilizada.

1. El JavaScript necesitamos agregarlo en el archivo store.js.tpl (o donde tengas tus funciones de JS).  Agregamos el siguiente código :

{# /* // Installments */ #}

{# Installments without interest #}

function get_max_installments_without_interests(number_of_installment, installment_data, max_installments_without_interests) {
    if (parseInt(number_of_installment) > parseInt(max_installments_without_interests[0])) {
        if (installment_data.without_interests) {
            return [number_of_installment, installment_data.installment_value.toFixed(2)];
        }
    }
    return max_installments_without_interests;
}

{# Installments with interest #}

function get_max_installments_with_interests(number_of_installment, installment_data, max_installments_with_interests) {
    if (parseInt(number_of_installment) > parseInt(max_installments_with_interests[0])) {
        if (installment_data.without_interests == false) {
            return [number_of_installment, installment_data.installment_value.toFixed(2)];
        }
    }
    return max_installments_with_interests;
}

{# Refresh installments inside detail popup #}

function refreshInstallmentv2(price){
    jQueryNuvem(".js-modal-installment-price" ).each(function( el ) {
        const installment = Number(jQueryNuvem(el).data('installment'));
        jQueryNuvem(el).text(LS.currency.display_short + (price/installment).toLocaleString('de-DE', {maximumFractionDigits: 2, minimumFractionDigits: 2}));
    });
}

{# Refresh price on payments popup with payment discount applied #}

function refreshPaymentDiscount(price){
    jQueryNuvem(".js-price-with-discount" ).each(function( el ) {
        const payment_discount = jQueryNuvem(el).data('paymentDiscount');
        jQueryNuvem(el).text(LS.formatToCurrency(price - ((price * payment_discount) / 100)))
    });
}

Ubicá la función function changeVariant y reemplazala por lo siguiente:El código que necesitamos para el carrito es el siguiente:

{# /* // Change variant */ #}

{# Updates price, installments, labels and CTA on variant change #}

function changeVariant(variant){

    jQueryNuvem(".js-product-detail .js-shipping-calculator-response").hide();
    jQueryNuvem("#shipping-variant-id").val(variant.id);

    var parent = jQueryNuvem("body");
    if (variant.element){
        parent = jQueryNuvem(variant.element);
    }

    var sku = parent.find('#sku');
    if(sku.length) {
        sku.text(variant.sku).show();
    }

    var installment_helper = function($element, amount, price){
        $element.find('.js-installment-amount').text(amount);
        $element.find('.js-installment-price').attr("data-value", price);
        $element.find('.js-installment-price').text(LS.currency.display_short + parseFloat(price).toLocaleString('de-DE', { minimumFractionDigits: 2 }));
        if(variant.price_short && Math.abs(variant.price_number - price * amount) < 1) {
            $element.find('.js-installment-total-price').text((variant.price_short).toLocaleString('de-DE', { minimumFractionDigits: 2 }));
        } else {
            $element.find('.js-installment-total-price').text(LS.currency.display_short + (price * amount).toLocaleString('de-DE', { minimumFractionDigits: 2 }));
        }
    };

    if (variant.installments_data) {
        var variant_installments = JSON.parse(variant.installments_data);
        var max_installments_without_interests = [0,0];
        var max_installments_with_interests = [0,0];
        for (let payment_method in variant_installments) {
            let installments = variant_installments[payment_method];
            for (let number_of_installment in installments) {
                let installment_data = installments[number_of_installment];
                max_installments_without_interests = get_max_installments_without_interests(number_of_installment, installment_data, max_installments_without_interests);
                max_installments_with_interests = get_max_installments_with_interests(number_of_installment, installment_data, max_installments_with_interests);
                var installment_container_selector = '#installment_' + payment_method.replace(" ", "_") + '_' + number_of_installment;


                if(!parent.hasClass("js-quickshop-container")){
                    installment_helper(jQueryNuvem(installment_container_selector), number_of_installment, installment_data.installment_value.toFixed(2));
                }
            }
        }
        var $installments_container = jQueryNuvem(variant.element + ' .js-max-installments-container .js-max-installments');
        var $installments_modal_link = jQueryNuvem(variant.element + ' #btn-installments');
        var $payments_module = jQueryNuvem(variant.element + ' .js-product-payments-container');
        var $installmens_card_icon = jQueryNuvem(variant.element + ' .js-installments-credit-card-icon');


        {% if product.has_direct_payment_only %}
        var installments_to_use = max_installments_without_interests[0] >= 1 ? max_installments_without_interests : max_installments_with_interests;

        if(installments_to_use[0] <= 0 ) {
        {%  else %}
        var installments_to_use = max_installments_without_interests[0] > 1 ? max_installments_without_interests : max_installments_with_interests;

        if(installments_to_use[0] <= 1 ) {
        {% endif %}
            $installments_container.hide();
            $installments_modal_link.hide();
            $payments_module.hide();
            $installmens_card_icon.hide();
        } else {
            $installments_container.show();
            $installments_modal_link.show();
            $payments_module.show();
            $installmens_card_icon.show();
            installment_helper($installments_container, installments_to_use[0], installments_to_use[1]);
        }
    }

    if(!parent.hasClass("js-quickshop-container")){
        jQueryNuvem('#installments-modal .js-installments-one-payment').text(variant.price_short).attr("data-value", variant.price_number);
    }

    if (variant.price_short){
        var variant_price_clean = variant.price_short.replace('$', '').replace('R', '').replace(',', '').replace('.', '');
        var variant_price_raw = parseInt(variant_price_clean, 10);
        parent.find('.js-price-display').text(variant.price_short).show();
        parent.find('.js-price-display').attr("content", variant.price_number).data('productPrice', variant_price_raw);
    } else {
        parent.find('.js-price-display').hide();
    }

    if ((variant.compare_at_price_short) && !(parent.find(".js-price-display").css("display") == "none")) {
        parent.find('.js-compare-price-display').text(variant.compare_at_price_short).show();
    } else {
        parent.find('.js-compare-price-display').hide();
    }

    var button = parent.find('.js-addtocart');
    button.removeClass('cart').removeClass('contact').removeClass('nostock');
    var $product_shipping_calculator = parent.find("#product-shipping-container");

    {# Update CTA wording and status #}

    {% if not store.is_catalog %}
    if (!variant.available){
        button.val('{{ "Sin stock" | translate }}');
        button.addClass('nostock');
        button.attr('disabled', 'disabled');
        $product_shipping_calculator.hide();
    } else if (variant.contact) {
        button.val('{{ "Consultar precio" | translate }}');
        button.addClass('contact');
        button.removeAttr('disabled');
        $product_shipping_calculator.hide();
    } else {
        button.val('{{ "Agregar al carrito" | translate }}');
        button.addClass('cart');
        button.removeAttr('disabled');
        $product_shipping_calculator.show();
    }

    {% endif %}

    {% if template == 'product' %}
        const base_price = Number(jQueryNuvem("#price_display").attr("content"));
        refreshInstallmentv2(base_price);
        refreshPaymentDiscount(variant.price_number);


        {% if settings.last_product and product.variations %}
            if(variant.stock == 1) {
                jQueryNuvem('.js-last-product').show();
            } else {
                jQueryNuvem('.js-last-product').hide();
            }
        {% endif %}
    {% endif %}

    {# Update shipping on variant change #}

    LS.updateShippingProduct();

    zipcode_on_changevariant = jQueryNuvem("#product-shipping-container .js-shipping-input").val();
    jQueryNuvem("#product-shipping-container .js-shipping-calculator-current-zip").text(zipcode_on_changevariant);
}

Por último agregá la siguiente función:

{# /* // Product labels on variant change */ #}

{# Stock, Offer and discount labels update #}

jQueryNuvem(document).on("change", ".js-variation-option", function(e) {

    var $parent = jQueryNuvem(this).closest(".js-product-variants");
    var $variants_group = jQueryNuvem(this).closest(".js-product-variants-group");
    var $quickshop_parent_wrapper = jQueryNuvem(this).closest(".js-quickshop-container");

    {# If quickshop is used from modal, use quickshop-id from the item that opened it #}
    
    var quick_id = $quickshop_parent_wrapper.attr("data-quickshop-id");

    if($parent.hasClass("js-product-quickshop-variants")){

        var $quickshop_parent = jQueryNuvem(this).closest(".js-item-product");

        {# Target visible slider item if necessary #}
        
        if($quickshop_parent.hasClass("js-item-slide")){
            var $quickshop_variant_selector = '.js-swiper-slide-visible .js-quickshop-container[data-quickshop-id="'+quick_id+'"]';
        }else{
            var $quickshop_variant_selector = '.js-quickshop-container[data-quickshop-id="'+quick_id+'"]';
        }
        
        LS.changeVariant(changeVariant, $quickshop_variant_selector);


        {% if settings.product_color_variants %}
            {# Match selected color variant with selected quickshop variant #}


            if(($variants_group).hasClass("js-color-variants-container")){
                var selected_option_id = jQueryNuvem(this).find("option:selected").val();
                if($quickshop_parent.hasClass("js-item-slide")){
                    var $color_parent_to_update = jQueryNuvem('.js-swiper-slide-visible .js-quickshop-container[data-quickshop-id="'+quick_id+'"]');
                }else{
                    var $color_parent_to_update = jQueryNuvem('.js-quickshop-container[data-quickshop-id="'+quick_id+'"]');
                }
                $color_parent_to_update.find('.js-color-variant, .js-insta-variant').removeClass("selected");
                $color_parent_to_update.find('.js-color-variant[data-option="'+selected_option_id+'"], .js-insta-variant[data-option="'+selected_option_id+'"]').addClass("selected");
            }
        {% endif %} 
    } else {
        LS.changeVariant(changeVariant, '#single-product');
    }

    {# Offer and discount labels update #}

    var $this_product_container = jQueryNuvem(this).closest(".js-product-container");


    if($this_product_container.hasClass("js-quickshop-container")){
        var this_quickshop_id = $this_product_container.attr("data-quickshop-id");
        var $this_product_container = jQueryNuvem('.js-product-container[data-quickshop-id="'+this_quickshop_id+'"]');
    }
    var $this_compare_price = $this_product_container.find(".js-compare-price-display");
    var $this_price = $this_product_container.find(".js-price-display");
    var $installment_container = $this_product_container.find(".js-product-payments-container");
    var $installment_text = $this_product_container.find(".js-max-installments-container");
    var $this_add_to_cart = $this_product_container.find(".js-prod-submit-form");


    // Get the current product discount percentage value
    var current_percentage_value = $this_product_container.find(".js-offer-percentage");


    // Get the current product price and promotional price
    var compare_price_value = $this_compare_price.html();
    var price_value = $this_price.html();


    // Calculate new discount percentage based on difference between filtered old and new prices
    const percentageDifference = window.moneyDifferenceCalculator.percentageDifferenceFromString(compare_price_value, price_value);
    if(percentageDifference){
        $this_product_container.find(".js-offer-percentage").text(percentageDifference);
        $this_product_container.find(".js-offer-label").css("display" , "table");
    }


    if ($this_compare_price.css("display") == "none" || !percentageDifference) {
        $this_product_container.find(".js-offer-label").hide();
    }


    if ($this_add_to_cart.hasClass("nostock")) {
        $this_product_container.find(".js-stock-label").show();
    }
    else {
        $this_product_container.find(".js-stock-label").hide();
    }
    if ($this_price.css('display') == 'none'){
        $installment_container.hide();
        $installment_text.hide();
    }else{
        $installment_text.show();
    }
});

2. Pero también necesitamos agregar el JS que hace funcionar al componente del modal en general y para las tabs

{#/*============================================================================
      #Modals
    ==============================================================================*/ #}

{# Full screen mobile modals back events #}

if (window.innerWidth < 768) {

    {# Clean url hash function #}

    cleanURLHash = function(){
        const uri = window.location.toString();
        const clean_uri = uri.substring(0, uri.indexOf("#"));
        window.history.replaceState({}, document.title, clean_uri);
    };

    {# Go back 1 step on browser history #}

    goBackBrowser = function(){
        cleanURLHash();
        history.back();
    };

    {# Clean url hash on page load: All modals should be closed on load #}

    if(window.location.href.indexOf("modal-fullscreen") > -1) {
        cleanURLHash();
    }

    {# Open full screen modal and url hash #}

    jQueryNuvem(document).on("click", ".js-fullscreen-modal-open", function(e) {
        e.preventDefault();
        var modal_url_hash = jQueryNuvem(this).data("modalUrl");
        window.location.hash = modal_url_hash;
    });

    {# Close full screen modal: Remove url hash #}

    jQueryNuvem(document).on("click", ".js-fullscreen-modal-close", function(e) {
        e.preventDefault();
        goBackBrowser();
    });

    {# Hide panels or modals on browser backbutton #}

    window.onhashchange = function() {
        if(window.location.href.indexOf("modal-fullscreen") <= -1) {

            {# Close opened modal #}

            if(jQueryNuvem(".js-fullscreen-modal").hasClass("modal-show")){

                {# Remove body lock only if a single modal is visible on screen #}

                if(jQueryNuvem(".js-modal.modal-show").length == 1){
                    jQueryNuvem("body").removeClass("overflow-none");
                }

                var $opened_modal = jQueryNuvem(".js-fullscreen-modal.modal-show");
                var $opened_modal_overlay = $opened_modal.prev();

                $opened_modal.removeClass("modal-show");
                setTimeout(() => $opened_modal.hide(), 500);
                $opened_modal_overlay.fadeOut(500);

  
            }
        }
    }
}

jQueryNuvem(document).on("click", ".js-modal-open", function(e) {
    e.preventDefault(); 
    var modal_id = jQueryNuvem(this).data('toggle');
    var $overlay_id = jQueryNuvem('.js-modal-overlay[data-modal-id="' + modal_id + '"]');
    if (jQueryNuvem(modal_id).hasClass("modal-show")) {
        let modal = jQueryNuvem(modal_id).removeClass("modal-show");
        setTimeout(() => modal.hide(), 500);
    } else {

        {# Lock body scroll if there is no modal visible on screen #}
        
        if(!jQueryNuvem(".js-modal.modal-show").length){
            jQueryNuvem("body").addClass("overflow-none");
        }
        $overlay_id.fadeIn(400);
        jQueryNuvem(modal_id).detach().appendTo("body");
        $overlay_id.detach().insertBefore(modal_id);
        jQueryNuvem(modal_id).show().addClass("modal-show");
    }             
});

jQueryNuvem(document).on("click", ".js-modal-close", function(e) {
    e.preventDefault();  
    {# Remove body lock only if a single modal is visible on screen #}

    if(jQueryNuvem(".js-modal.modal-show").length == 1){
        jQueryNuvem("body").removeClass("overflow-none");
    }
    var $modal = jQueryNuvem(this).closest(".js-modal");
    var modal_id = $modal.attr('id');
    var $overlay_id = jQueryNuvem('.js-modal-overlay[data-modal-id="#' + modal_id + '"]');
    $modal.removeClass("modal-show");
    setTimeout(() => $modal.hide(), 500);
    $overlay_id.fadeOut(500);

    {# Close full screen modal: Remove url hash #}

    if ((window.innerWidth < 768) && (jQueryNuvem(this).hasClass(".js-fullscreen-modal-close"))) {
        goBackBrowser();
    }    
});

jQueryNuvem(document).on("click", ".js-modal-overlay", function(e) {
    e.preventDefault();
    {# Remove body lock only if a single modal is visible on screen #}

    if(jQueryNuvem(".js-modal.modal-show").length == 1){
        jQueryNuvem("body").removeClass("overflow-none");
    }
    var modal_id = jQueryNuvem(this).data('modalId');
    let modal = jQueryNuvem(modal_id).removeClass("modal-show");
    setTimeout(() => modal.hide(), 500); 
    jQueryNuvem(this).fadeOut(500);   


    if (jQueryNuvem(this).hasClass("js-fullscreen-overlay") && (window.innerWidth < 768)) {
        cleanURLHash();
    }
});

3. Como en este tutorial usamos la técnica de lazy load con el plugin Lazysizes, necesitamos agregarlo. Para ver como hacerlo podés leer este corto artículo y luego continuar con este tutorial.

Traducciones

Para terminar agregamos los textos para las traducciones en el archivo config/translations.txt

es "Ver medios de pago"
pt "Ver meios de pagamento"
en "See payment options"
es_mx "Ver métodos de pago"

Listo, ya tenés en tu diseño la funcionalidad aplicada ¡Excelente!

Nuevas actualizaciones disponibles