Calculadora de frete

No tutorial a seguir explicamos como adicionar o calculador de frete e as lojas físicas em seu layout:

O código neste tutorial inclui:

  • A calculador de frete, tanto no detalhe do produto como no carrinho
  • O filtro entre opções de envio semelhantes. Isso destaca a melhor opção de envio e oculta o restante em um link "Ver mais opções". Não se aplica a meios de envio personalizados.
  • Datas exatas de envio em vez do tempo estimado em dias
  • Select de lojas físicas (somente para lojas não brasileiras)
  • Mensagem sobre frete grátis (aplica-se apenas a lojas no Brasil que usam o meio de envio Correios)

Abaixo do subtotal você pode ver um exemplo da calculadora

À esquerda, as opções de envio exibidas e à direita as opções menos relevantes, visíveis clicando em "Ver mais opções".

HTML

A primeira coisa que vamos fazer é criar os tpls necessários para a funcionalidade.

1. Adicionamos a pasta com o nome shipping dentro da pasta snipplets

2. Dentro desta pasta, criamos os seguintes arquivos, cada um com seu respectivo código. É importante manter os IDs e classes "js -..." para garantir sua operação quando adicionamos JavaScript.

branches.tpl

Esse arquivo representa o seletor de lojas físicas que são visíveis apenas para lojas não brasileiras. O código é o seguinte:

<div class="js-toggle-branches w-100">
    <span class="form-row">
        <div class="col-auto">
            {% include "snipplets/svg/store.tpl" with {svg_custom_class: "icon-inline icon-lg link-module-icon svg-icon-text"} %}
        </div>
        <div class="col-6">
            <div {% if store.branches|length > 1 %}class="mb-1"{% endif %}> 
                {% if store.branches|length > 1 %}
                    {{ 'Nuestros locales' | translate }}
                {% else %}
                    {{ 'Nuestro local' | translate }}
                {% endif %}
            </div>
            {% if store.branches|length > 1 %}
                <div class="btn-link float-left">
                    <span class="js-see-branches">
                        {{ 'Ver opciones' | translate }}
                    </span>
                    {% include "snipplets/svg/chevron-down.tpl" with {svg_custom_class: "js-see-branches icon-inline ml-1"} %}
                    <span class="js-hide-branches" style="display: none;">
                        {{ 'Ocultar opciones' | translate }}
                        {% include "snipplets/svg/chevron-up.tpl" with {svg_custom_class: "icon-inline ml-1"} %}
                    </span>
                </div>
            {% endif %}
        </div>
    </span>
</div>

{# Store branches #}

{% if not product_detail %}
    
    <ul class="js-store-branches-container list-unstyled radio-button-container mt-4" {% if store.branches|length > 1 %}style="display: none;"{% endif %}>

        {# Selectable branches #}

        {% for branch in store.branches %}
            <li class="radio-button-item">
                <label class="js-shipping-radio js-branch-radio radio-button" data-loop="branch-radio-{{loop.index}}">
                <input 
                    class="js-branch-method {% if cart.shipping_data.code == branch.code %} js-selected-shipping-method {% endif %} shipping-method" 
                    data-price="0" 
                    {% if cart.shipping_data.code == branch.code %}checked{% endif %} type="radio" 
                    value="{{branch.code}}" 
                    data-name="{{ branch.name }} - {{ branch.extra }}"
                    data-code="{{branch.code}}" 
                    data-cost="{{ 'Gratis' | translate }}"
                    name="option" 
                    style="display:none">
                    <span class="shipping-option row-fluid radio-button-content">
                       <span class="radio-button-icons">
                            <span class="radio-button-icon unchecked"></span>
                            <span class="radio-button-icon checked"></span>
                            <span class="radio-button-icon checked checked-invert"></span>
                        </span>
                        <span class="radio-button-label">
                            <h6 class="text-primary mb-1 d-inline-block">{{ 'Gratis' | translate }}</h6>
                            <span class="radio-button-text">
                                {{ branch.name }} - {{ branch.extra }}
                            </span>
                        </span>
                    </span>
                </label>
            </li>
        {% endfor %}
    </ul>
{% else %}
    <ul class="js-store-branches-container list-unstyled list mt-4" {% if store.branches|length > 1 %}style="display: none;"{% endif %}>
        {% for branch in store.branches %}
            <li class="list-item">
                <span class="list-item-content">
                    <h6 class="text-primary mb-1">{{ 'Gratis' | translate }}</h6>
                    <div>{{ branch.name }} - {{ branch.extra }}</div>
                </span>
            </li>
        {% endfor %}
    </ul>
{% endif %}

shipping-calculator.tpl

É o componente da calculadora de frete. Inclui um input, um botão, a mensagem de erro e uma mensagem que mostra o valor mínimo para obter frete grátis, bem como uma mensagem notificando que o frete grátis foi atingido quando excedido esse valor (isso se aplica somente a lojas que usam Correios).

<div class="{% if product_detail %}product-shipping-calculator{% endif %} mb-2 w-100" data-store="shipping-calculator">

    <div class="js-shipping-calculator-head shipping-calculator-head position-relative transition-soft {% if cart.shipping_zipcode %}with-zip{% else %}with-form{% endif %}">
        <div class="js-shipping-calculator-with-zipcode {% if cart.shipping_zipcode %}js-cart-saved-zipcode transition-up-active{% endif %} mt-3 mb-4 w-100 transition-up position-absolute">
            <div class="container p-0">
                <div class="row align-items-center">
                    <span class="col pr-0">
                        <span class="font-small align-sub">
                            <span>{{ "Entregas para el CP:" | translate }}</span>
                            <strong class="js-shipping-calculator-current-zip">{{ cart.shipping_zipcode }}</strong>
                        </span>
                    </span>
                    <div class="col-auto pl-0">
                        <a class="js-shipping-calculator-change-zipcode btn btn-secondary btn-small float-right py-1 px-2 px-sm-3" href="#">{{ "Cambiar CP" | translate }}</a>
                    </div>
                </div>
            </div>
        </div>
        <div class="js-shipping-calculator-form shipping-calculator-form transition-up position-absolute">

            {# Shipping calcualtor input #}
            
            {% embed "snipplets/forms/form-input.tpl" with{type_tel: true, input_value: cart.shipping_zipcode, input_name: 'zipcode', input_custom_class: 'js-shipping-input', input_placeholder: "Tu código postal" | translate, input_aria_label: 'Tu código postal' | translate, input_label: false, input_append_content: true, input_group_custom_class: 'form-row align-items-center mb-3', form_control_container_custom_class: 'col-5'} %}
                {% block input_prepend_content %}
                    <div class="col-12 mb-2">

                        {% include "snipplets/svg/truck.tpl" with {svg_custom_class: "icon-inline icon-w-18 icon-lg svg-icon-text mr-2"} %}

                        {# Free shipping achieved label #}

                        <span class="js-free-shipping-message font-weight-bold text-accent" {% if not cart.free_shipping.cart_has_free_shipping %}style="display: none;"{% endif %}>
                            {{ "¡Genial! Tenés envío gratis" | translate }}
                        </span>

                        {# Free shipping with min price label #}

                        <span class="js-shipping-calculator-label font-weight-bold" {% if cart.free_shipping.cart_has_free_shipping or not cart.free_shipping.min_price_free_shipping.min_price %}style="display: none;"{% endif %}>
                            {{ "<strong class='text-accent'>Envío gratis</strong> superando los" | translate }} <span>{{ cart.free_shipping.min_price_free_shipping.min_price }}</span>
                        </span>

                        {# Shipping default label #}

                        <span class="js-shipping-calculator-label-default" {% if cart.free_shipping.cart_has_free_shipping or cart.free_shipping.min_price_free_shipping.min_price %}style="display: none;"{% endif %}>

                            {# Regular shipping calculator label #}
                            
                            {{ 'Medios de envío' | translate }}
                        </span>
                    </div>
                {% endblock input_prepend_content %}
                {% block input_form_alert %}
                {% if store.country == 'BR' or 'AR' or 'MX' %}
                    {% set zipcode_help_ar = 'https://www.correoargentino.com.ar/formularios/cpa' %}
                    {% set zipcode_help_br = 'http://www.buscacep.correios.com.br/sistemas/buscacep/' %}
                    {% set zipcode_help_mx = 'https://www.correosdemexico.gob.mx/datosabiertos/gobmx/gobmx_Descarga.html' %}
                    <div class="col-12">
                        <a class="font-small text-primary mt-3 mb-2 d-block" href="{% if store.country == 'AR' %}{{ zipcode_help_ar }}{% elseif store.country == 'BR' %}{{ zipcode_help_br }}{% elseif store.country == 'MX' %}{{ zipcode_help_mx }}{% endif %}" target="_blank">{{ "No sé mi código postal" | translate }}</a>
                    </div>
                {% endif %}
                <div class="col-12">
                    <div class="js-ship-calculator-error invalid-zipcode alert alert-danger" style="display: none;">
                        {# Specific error message considering if store has multiple languages #}


                        {% for language in languages %}
                            {% if language.active %}
                                {% if languages | length > 1 %}
                                    {% set wrong_zipcode_wording = ' para ' | translate ~ language.country_name ~ '. Podés intentar con otro o' | translate %}
                                {% else %}
                                    {% set wrong_zipcode_wording = '. ¿Está bien escrito?' | translate %}
                                {% endif %}
                                {{ "No encontramos este código postal{1}" | translate(wrong_zipcode_wording) }}


                                {% if languages | length > 1 %}
                                    <a href="#" data-toggle="#{% if product_detail %}product{% else %}cart{% endif %}-shipping-country" class="js-modal-open btn-link btn-link-primary text-lowercase">
                                        {{ 'cambiar tu país de entrega' | translate }}
                                    </a>
                                {% endif %}
                            {% endif %}
                        {% endfor %}
                    </div>
                    <div class="js-ship-calculator-error js-ship-calculator-common-error alert alert-danger" style="display: none;">{{ "Ocurrió un error al calcular el envío. Por favor intentá de nuevo en unos segundos." | translate }}</div>
                    <div class="js-ship-calculator-error js-ship-calculator-external-error alert alert-danger" style="display: none;">{{ "El calculo falló por un problema con el medio de envío. Por favor intentá de nuevo en unos segundos." | translate }}</div>
                </div>
                {% endblock input_form_alert %}
                {% block input_append_content %}
                <span class="col-6">
                    <button class="js-calculate-shipping btn btn-default btn-block" aria-label="{{ 'Calcular envío' | translate }}">    
                        <span class="js-calculate-shipping-wording">{{ "Calcular" | translate }}</span>
                        <span class="js-calculating-shipping-wording" style="display: none;">{{ "Calculando" | translate }}</span>
                    </button>
                    {% if shipping_calculator_variant %}
                        <input type="hidden" name="variant_id" id="shipping-variant-id" value="{{ shipping_calculator_variant.id }}">
                    {% endif %}
                </span>
                {% endblock input_append_content %}
            {% endembed %}
        </div>
    </div>
    <div class="js-shipping-calculator-spinner shipping-spinner-container mb-3 float-left w-100 transition-soft text-center" style="display: none;">
        <div class="spinner-ellipsis">
            <div class="point"></div>
            <div class="point"></div>
            <div class="point"></div>
            <div class="point"></div>
        </div>
    </div>
    <div class="js-shipping-calculator-response mb-3 float-left w-100 {% if product_detail %}list list-readonly{% endif %}" style="display: none;"></div>
</div>

{# Shipping country modal #}

{% if languages | length > 1 %}

    {% if product_detail %}
        {% set country_modal_id = 'product-shipping-country' %}
    {% else %}
        {% set country_modal_id = 'cart-shipping-country' %}
    {% endif %}


    {% embed "snipplets/modal.tpl" with{modal_id: country_modal_id, modal_class: 'bottom modal-centered-small js-modal-shipping-country', modal_position: 'center', modal_transition: 'slide', modal_header: true, modal_footer: true, modal_width: 'centered', modal_zindex_top: true, modal_mobile_full_screen: false} %}
        {% block modal_head %}
            {{ 'País de entrega' | translate }}
        {% endblock %}
        {% block modal_body %}
            {% embed "snipplets/forms/form-select.tpl" with{select_label: true, select_label_name: 'País donde entregaremos tu compra' | translate, select_aria_label: 'País donde entregaremos tu compra' | translate, select_custom_class: 'js-shipping-country-select', select_group_custom_class: 'mt-4' } %}
                {% block select_options %}
                    {% for language in languages %}
                        <option value="{{ language.country }}" data-country-url="{{ language.url }}" {% if language.active %}selected{% endif %}>{{ language.country_name }}</option>
                    {% endfor %}
                {% endblock select_options%}
            {% endembed %}
        {% endblock %}
        {% block modal_foot %}
            <a href="#" class="js-save-shipping-country btn btn-primary float-right">{{ 'Aplicar' | translate }}</a>
        {% endblock %}
    {% endembed %}
{% endif %}

shipping-calculator-item.tpl

Este é o item na lista de opções de envio. Dentro disso você pode mostrar:

VariávelDescrição
option.short_nameNome do meio de envío utilizando option.short_name
option.costPreço com option.cost, se option.cost == 0 é exibido como gratis.
option.old_costO preço antigo quando é uma opção de Correios com frete grátis, con option.old_cost
option.timeTempo formatado em dias, por exemplo, "5 dias úteis"
option.timeTempo formatado em datas, por exemplo “Chega entre terça-feira 07/08 e quarta-feira 09/08”. Recomendamos usar essa opção, pois é mais fácil para o usuário entender.
option.payment_rulesUma mensagem se o meio de envio estiver disponível apenas para um determinado meio de pagamento usando option.payment_rules

Se o meio de envio tiver subopções, incluímos o tpl da pasta shipping_suboptions (que adicionaremos mais tarde). Aplica-se apenas a lojas na Argentina com o meio de remessa OCA por enquanto.

Também tem uma mensagem de alerta no caso de precisarmos mostrar alguma condição extra no meio de envio. Isso é definido a partir do PHP Nuvemshop.

Além disso o condicional feature_option no radio button. Isso serve para diferenciar as opções de frete pendentes da menor prioridade ao incluí-las no tpl shipping_options.tpl (que serão incluídas posteriormente). Lembre-se de que essa filtragem não inclui meios de entrega locais ou personalizados.

O código é o seguinte:

<li class="js-shipping-list-item radio-button-item">
    <label class="js-shipping-radio radio-button list-item" data-loop="shipping-radio-{{loop.index}}">
        <input 
        id="{% if featured_option %}featured-{% endif %}shipping-{{loop.index}}" 
        class="js-shipping-method {% if not featured_option %}js-shipping-method-hidden{% endif %} shipping-method" 
        data-price="{{option.cost.value}}" 
        data-code="{{option.code}}" 
        data-name="{{option.name}}" 
        data-cost="{% if option.show_price %} {% if option.cost.value == 0  %}{{ 'Gratis' | translate }}{% else %}{{option.cost}}{% endif %}{% else %} {{ 'A convenir' | translate }} {% endif %}" 
        type="radio" 
        value="{{option.code}}" 
        {% if featured_option and loop.first %}checked="checked"{% endif %} name="option" 
        style="display:none" />
        <span class="radio-button-content">
            <span class="radio-button-icons">
                <span class="radio-button-icon unchecked"></span>
                <span class="radio-button-icon checked"></span>
            </span>
            <span class="radio-button-label">


                {# Improved shipping option with no carrier img and ordered shipping info #}
                
                <div class="radio-button-text"> 
                    {% if option.show_price %} 
                        <div class="mb-1 d-inline-block">
                            <span class="text-primary h6">
                                {% if option.cost.value == 0  %}
                                    {{ 'Gratis' | translate }}
                                {% else %}
                                    {{option.cost}}
                                {% endif %}
                            </span>
                            {% if option.cost.value == 0 and option.old_cost.value %}
                                <span class="price-compare text-foreground font-small ml-1">{{option.old_cost}}</span>
                            {% endif %}
                        </div>
                    {% endif %}
                    {% if option.time %}
                        <div>
                            <strong>
                            {% if store.has_smart_dates %}
                                {{option.dates}}
                            {% else %}
                                {{option.time}}
                            {% endif %}
                            </strong>
                        </div>
                    {% endif %}
                </div>
                <div class="radio-button-text">
                    {{option.short_name}} {{ option.method == 'branch'  ? option.extra.extra  :  '' }}
                </div>
                {% if option.payment_rules %}
                    <div>
                        {% include "snipplets/svg/info-circle.tpl" with {svg_custom_class: "icon-inline svg-icon-text"} %}
                        <i>{{option.payment_rules}}</i>
                    </div>
                {% endif %}


                {% if option.suboptions is not empty %}
                    {% include "snipplets/shipping_suboptions/#{option.suboptions.type}.tpl" with {'suboptions': option.suboptions} %}
                {% endif %}


                {% if option.warning['enable'] %}
                    <div class="alert alert-warning">
                      <p>{{ option.warning['message'] }}</p>
                    </div>
                {% endif %}
            </span>
        </span>
    </label>
</li>

3. Criamos o arquivo shipping_options.tpl dentro da pasta snipplets. Você não deve mudar seu nome ou localização, já que usamos o Nuvemshop PHP para exibi-lo dentro do div com a classe js-shipping-calculator-response na tpl shipping-calculator.tpl

Dentro deste snipplet são construídas as duas listas de opções de envio, a priorizada e a oculta no link “Ver mais opções”.

{% if options %}

    {% if store.show_shipping_emergency_message %}
        <div class="alert alert-warning">{{ store.shipping_emergency_message }}</div> 
    {% endif %}

    <div class="{% if cart.items_count > 0 %}js-product-shipping-label{% endif %} font-small mb-4 pb-1" style="display: none;">
        {{ 'Opciones para tu compra <strong>si sumás este producto</strong>.' | translate }}
    </div>

    {# Check for only shipping featured options #}

    {% set has_featured_shipping = false %}

    {% for option in options_to_show if option.shipping_type == 'ship' or option.shipping_type == 'delivery' or (option.method == 'table' and option.shipping_type == 'custom') %}
        {% if option |length >= 1 %}
            {% set has_featured_shipping = true %}
        {% endif %}
    {% endfor %}

    {# Check for only non featured shipping options #}

    {% set has_non_featured_shipping = false %}

    {% for option in options_to_hide if option.shipping_type == 'ship' or option.shipping_type == 'delivery' or (option.method == 'table' and option.shipping_type == 'custom') %}
        {% if option |length >= 1 %}
            {% set has_non_featured_shipping = true %}
        {% endif %}
    {% endfor %}

    {# Pickup featured options #}

    {% set has_non_featured_pickup = false %}
    {% set has_featured_pickup = false %}

    {# Check for only pickup featured options #}

    {% for option in options_to_show if option.shipping_type == 'pickup' and option.method != 'branch' %}
        {% if option |length >= 1 %}
            {% set has_featured_pickup = true %}
        {% endif %}
    {% endfor %}

    {# Check for only non featured pickup options #}

    {% for option in options_to_hide if option.shipping_type == 'pickup' and option.method != 'branch' %}
        {% if option |length >= 1 %}
            {% set has_non_featured_pickup = true %}
        {% endif %}
    {% endfor %}

    {# Shipping options #}

    {% if has_featured_shipping %}

        <div class="full-width-container {% if has_featured_pickup %}mb-4{% endif %}">

            <div class="form-label mb-2">
                {% include "snipplets/svg/truck.tpl" with {svg_custom_class: "icon-inline icon-lg svg-icon-text mr-2 align-bottom"} %}
                {{ "Envío a domicilio" | translate }}
            </div>

            <ul class="box radio-button-container p-0 mb-0 list-unstyled">

                {# Smart shipping hides similar shipping options on a toggle div and also shows an improved shipping item #}

                {# Check if smart shipping is needed #}

                {# Include branch options inside calculador #}

                {% for option in options_to_show if option.shipping_type == 'ship' or option.shipping_type == 'delivery' or (option.method == 'table' and option.shipping_type == 'custom') %}
                    {% include "snipplets/shipping/shipping-calculator-item.tpl" with {'featured_option': true} %}
                {% endfor %}

                {% if has_non_featured_shipping %}

                    <div class="js-other-shipping-options w-100 float-left shipping-extra-options" style="display: none;">

                        {# Smart shipping hides similar shipping options on a toggle div and also shows an improved shipping item #}

                        {# Check if smart shipping is needed #}

                        {# Include branch options inside calculador #}

                        {% for option in options_to_hide if option.shipping_type == 'ship' or option.shipping_type == 'delivery' or (option.method == 'table' and option.shipping_type == 'custom') %}
                            {% include "snipplets/shipping/shipping-calculator-item.tpl" %}
                        {% endfor %}
                    </div>
             
                {% endif %}

            </ul>

            {% if has_non_featured_shipping %}
                <div class="js-toggle-more-shipping-options js-show-more-shipping-options w-100 float-left text-center mt-2">
                    <a href="#" class="btn-link">
                        <span class="js-shipping-see-more">
                            {{ 'Ver más opciones de envío' | translate }}
                        </span>
                        <span class="js-shipping-see-less" style="display: none;">
                            {{ 'Ver menos opciones de envío' | translate }}
                        </span>
                    </a>
                </div>
            {% endif %}
        </div>

    {% endif %}

    {# Pickup featured options #}

    {% if has_featured_pickup %}

        <div class="full-width-container mb-2">

            <div class="form-label mb-2">
                {% include "snipplets/svg/map-marker-alt.tpl" with {svg_custom_class: "icon-inline icon-lg svg-icon-text mr-2 align-bottom"} %}
                {{ "Retirar por" | translate }}
            </div>

            <ul class="list-unstyled box radio-button-container p-0 mb-0">

                {# Smart shipping hides similar shipping options on a toggle div and also shows an improved shipping item #}

                {# List only pickup featured options #}

                {% for option in options_to_show if option.shipping_type == 'pickup' and option.method != 'branch' %}
                    {% include "snipplets/shipping/shipping-calculator-item.tpl" with {'featured_option': true, 'pickup' : true} %}
                {% endfor %}

                {% if has_non_featured_pickup %}

                    <div class="js-other-pickup-options w-100 float-left shipping-extra-options" style="display: none;">

                        {# Smart shipping hides similar shipping options on a toggle div and also shows an improved shipping item #}

                        {# List only pickup featured options: same logic as for featured pickups but for non featured #}

                        {% for option in options_to_hide if option.shipping_type == 'pickup' and option.method != 'branch' %}
                            {% include "snipplets/shipping/shipping-calculator-item.tpl" with {'pickup' : true}  %}
                        {% endfor %}
                    </div>
                {% endif %}
            </ul>

            {% if has_non_featured_pickup %}
                <div class="js-toggle-more-shipping-options js-show-other-pickup-options w-100 float-left text-center mt-2">
                    <a href="#" class="btn-link">
                        <span class="js-shipping-see-more">
                            {{ 'Ver más opciones de retiro' | translate }}
                        </span>
                        <span class="js-shipping-see-less" style="display: none;">
                            {{ 'Ver menos opciones de retiro' | translate }}
                        </span>
                    </a>
                </div>
            {% endif %}
        </div>

    {% endif %}
    {% if store.has_smart_dates and show_time %}
        <div class="font-small float-left w-100 mb-3">{{"El tiempo de entrega <strong>no considera feriados</strong>." | translate}}</div>
    {% endif %}

{% else %}
<span>{{"No hay costos de envío para el código postal dado." | translate}}</span>
{% endif %}

{# Don't remove this #}
<input type="hidden" name="after_calculation" value="1"/>
<input type="hidden" name="zipcode" value="{{zipcode}}"/>

4. Adicionamos a pasta shipping_suboptions dentro da pasta snipplet e finalmente criamos um tpl chamado select.tpl.

Isso mostra subopções na forma de um select para os meios de frete que precisam ser exibidos. Por enquanto, ela só se aplica a agências OCA.

{% set selected_option = loop.first or cart.shipping_option == option.name %}
<div class="js-shipping-suboption {{suboptions.name}}">
    {% if suboptions.options %}

        {# Read only suboptions inside popup #}

        {% set modal_id_val = (suboptions.name | sanitize) ~ '-pickup-modal-' ~ random() %}

        <div data-toggle="#{{ modal_id_val }}" class="js-modal-open mt-2">
            {% include "snipplets/svg/map-marker-alt.tpl" with {svg_custom_class: "icon-inline icon-lg mr-1"} %}
            <span class="btn-link btn-link-primary align-bottom">{{ 'Ver puntos de retiro' | translate }}</span>
        </div>

        {% embed "snipplets/modal.tpl" with{modal_id: modal_id_val, modal_class: 'bottom modal-centered-small js-modal-shipping-suboptions', modal_position: 'center', modal_transition: 'slide', modal_header: true, modal_footer: false, modal_width: 'centered', modal_zindex_top: true} %}
            {% block modal_head %}
                {{ 'Puntos de retiro' | translate }}
            {% endblock %}
            {% block modal_body %}
                <ul class="list-unstyled py-2">
                    {% for option in suboptions.options %}
                        <li class="text-capitalize mb-3">{% include "snipplets/svg/map-marker-alt.tpl" with {svg_custom_class: "icon-inline svg-icon-primary d-flex float-left mr-2"} %} <span class="d-flex">{{ option.name | lower }}</span></li>
                    {% endfor %}
                </ul>
                <div class="mt-4"><span class="opacity-50">{{ 'Cercanos al CP:'}}</span> <span class="text-primary font-weight-bold">{{cart.shipping_zipcode}}</span></div>
                <div class="mt-2 font-small">
                    {% include "snipplets/svg/info-circle.tpl" with {svg_custom_class: "icon-inline svg-icon-text"} %}
                    <i>{{ "Vas a poder elegir estas opciones antes de finalizar tu compra" | translate }}</i>
                </div>
            {% endblock %}
        {% endembed %}
    {% else %}
        <input type="hidden" name="{{suboptions.name}}"/>
        <div>{{ suboptions.no_options_message | translate }}</div>
    {% endif %}
</div>

5. Agora precisamos chamar a calculadora de remessa e as lojas no carrinho e no detalhe do produto.

Para o detalhe do produto, adicionamos o seguinte código no product-form.tpl ou no arquivo em que você tem o formulário do produto:

{% set show_product_fulfillment = settings.shipping_calculator_product_page and (store.has_shipping or store.branches) and not product.free_shipping and not product.is_non_shippable %}

{% if show_product_fulfillment %}

    <div class="divider"></div>

    {# Shipping calculator and branch link #}

    <div id="product-shipping-container" class="product-shipping-calculator list" {% if not product.display_price or not product.has_stock %}style="display:none;"{% endif %} data-shipping-url="{{ store.shipping_calculator_url }}">

        {# Shipping Calculator #}
        
        {% if store.has_shipping %}
            {% include "snipplets/shipping/shipping-calculator.tpl" with {'shipping_calculator_variant' : product.selected_or_first_available_variant, 'product_detail': true} %}
        {% endif %}

        {% if store.branches %}
            
            {# Link for branches #}
            {% include "snipplets/shipping/branches.tpl" with {'product_detail': true} %}
        {% endif %}
    </div>
    <div class="divider"></div>
{% endif %} 

Em seguida, temos que adicionar a calculadora no carrinho, no layout Base é no snipplet cart-totals.tpl mas em seu projeto pode ser em cart.tpl ou cart-panel-ajax.tpl

{# Define contitions to show shipping calculator and store branches on cart #}

{% set show_calculator_on_cart = settings.shipping_calculator_cart_page and store.has_shipping %}
{% set show_cart_fulfillment = settings.shipping_calculator_cart_page and (store.has_shipping or store.branches) %}

{% if show_cart_fulfillment %}
  <div class="js-fulfillment-info js-allows-non-shippable" {% if not cart.has_shippable_products %}style="display: none"{% endif %}>
    <div class="js-visible-on-cart-filled divider {% if cart_page %}d-md-none{% endif %}" {% if cart.items_count == 0 %}style="display:none;"{% endif %}></div>

      <div class="js-visible-on-cart-filled js-has-new-shipping js-shipping-calculator-container container-fluid">

        {# Saved shipping not available #}

        <div class="js-shipping-method-unavailable alert alert-warning row" style="display: none;">
          <div>
            <strong>{{ 'El medio de envío que habías elegido ya no se encuentra disponible para este carrito. ' | translate }}</strong>
          </div>
          <div>
            {{ '¡No te preocupes! Podés elegir otro.' | translate}}
          </div>
        </div>

        {# Shipping calculator and branch link #}

        <div id="cart-shipping-container" class="row" {% if cart.items_count == 0 %} style="display: none;"{% endif %} data-shipping-url="{{ store.shipping_calculator_url }}">

          {# Used to save shipping #}

          <span id="cart-selected-shipping-method" data-code="{{ cart.shipping_data.code }}" class="hidden">{{ cart.shipping_data.name }}</span>

          {# Shipping Calculator #}

          {% if store.has_shipping %}
            {% include "snipplets/shipping/shipping-calculator.tpl" with {'product_detail': false} %}
          {% endif %}

          {# Store branches #}

          {% if store.branches %}

            {# Link for branches #}

            {% include "snipplets/shipping/branches.tpl" with {'product_detail': false} %}
          {% endif %}
        </div>
      </div>

      <div class="js-visible-on-cart-filled divider {% if cart_page %}d-md-none{% endif %} {% if not store.branches %} mt-0{% endif %}" {% if cart.items_count == 0 %}style="display:none;"{% endif %}></div>
    </div>
  </div>
{% endif %}

6. Precisamos adicionar no template product.tpl no div pai de todo o detalhe do producto, o ID “single-product” e as classes js-has-new-shipping js-product-detail js-product-container, js-shipping-calculator-container. Ficando da seguinte maneira:

<div id="single-product" class="js-has-new-shipping js-product-detail js-product-container js-shipping-calculator-container" data-variants="{{product.variants_object | json_encode }}" itemscope itemtype="http://schema.org/Product">
HTML do detalhe de produto
</div>

7. Criamos a pasta forms dentro da pasta snipplets. Aqui precisamos adicionar um novo arquivo com o nome form-input.tpl que usaremos para o input do calculador.

{# /*============================================================================
  #Form input
==============================================================================*/

#Properties

#Group
    //input_group_custom_class for custom CSS classes
#Label 
    // input_label_id for ID
    // input_for for label for
    // input_label_custom_class for custom CSS classes
    // input_label_text for label text
#Prepend
    // input_prepend_content to add content before input
#Container (Only if has prepend or append)
    // form_control_container_custom_class for container custom class. E.g: col
#Input 
    // Can be text_area or input
    // input_type to define type (text, tel, number or passowrd)
    // input_id for id
    // input_name for name
    // input_value for val
    // input_placeholder for placeholder
    // input_custom_class for custom CSS classes 
    // input_rows for textarea rows
    // input_data_attr for data attributes
    // input_data_val for input_data_attr value
    // input_aria_label for aria-label attribute
#Append
    // input_append_content to add content after input
#Alerts 
    // input_form_alert to insert alerts
#}

<div class="form-group {{ input_group_custom_class }}">
    {% if input_label_text %}
        <label {% if input_label_id %}id="{{ input_label_id }}"{% endif %} class="form-label {{ input_label_custom_class }}" {% if input_for %}for="{{ input_name }}"{% endif %}>{{ input_label_text }}</label>
    {% endif %}
    {% block input_prepend_content %}
    {% endblock input_prepend_content %}
    {% if input_append_content or input_prepend_content %}
    <div class="form-control-container {{ form_control_container_custom_class }}">
    {% endif %}
    {% if text_area %}
        <textarea
            {% if input_id %}id="{{ input_id }}"{% endif %}
            class="form-control form-control-area {{ input_custom_class }} {% if input_append_content %}form-control-inline{% endif %}" 
            autocorrect="off" 
            autocapitalize="off" 
            {% if input_name %}name="{{ input_name }}"{% endif %}
            {% if input_value %}value="{{ input_value }}"{% endif %}
            {% if input_rows %}rows="{{ input_rows }}"{% endif %}
            {% if input_placeholder %}placeholder="{{ input_placeholder }}"{% endif %}
            {% if input_data_attr %}data-{{ input_data_attr }}="{{ input_data_val }}"{% endif %}></textarea>
    {% else %}
        <input 
            type="{% if type_text %}text{% elseif type_number %}number{% elseif type_tel %}tel{% elseif type_password %}password{% elseif type_hidden %}hidden{% endif %}"
            {% if input_id %}id="{{ input_id }}"{% endif %}
            class="form-control {{ input_custom_class }} {% if input_append_content %}form-control-inline{% endif %}" 
            autocorrect="off" 
            autocapitalize="off" 
            {% if type_password %}autocomplete="off"{% endif %}
            {% if input_name %}name="{{ input_name }}"{% endif %}
            {% if input_value %}value="{{ input_value }}"{% endif %}
            {% if input_min %}min="{{ input_min }}"{% endif %}
            {% if input_placeholder %}placeholder="{{ input_placeholder }}"{% endif %}
            {% if input_data_attr %}data-{{ input_data_attr }}="{{ input_data_val }}"{% endif %}
            {% if input_aria_label %}aria-label="{{ input_aria_label }}"{% endif %}/>
    {% endif %}
    {% if input_append_content or input_prepend_content %}
    </div>
    {% endif %}
    {% block input_append_content %}
    {% endblock input_append_content %}
    {% if input_help %}
    <div class="mt-4 text-center">
        <a href="{{ input_help_link }}" class="btn-link {{ input_link_class }}">{% block input_help_text %}{% endblock input_help_text %}</a>
    </div>
    {% endif %}
    {% block input_form_alert %}
    {% endblock input_form_alert %}
</div>

8. Finalmente, para a parte HTML, precisamos adicionar uma pasta SVG dentro da pasta snipplets. Aqui vamos adicionar os SVGs que usamos para os ícones na calculadora.

store.tpl

<svg class="{{ svg_custom_class }}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 616 512"><path d="M602 118.6L537.1 15C531.3 5.7 521 0 510 0H106C95 0 84.7 5.7 78.9 15L14 118.6c-29.6 47.2-10 110.6 38 130.8v227.4c0 19.4 14.3 35.2 32 35.2h448c17.7 0 32-15.8 32-35.2V249.4c48-20.2 67.6-83.6 38-130.8zM516 464H100v-96h416zm-.2-144.2H100v-64.7c24-3.3 45.1-15.2 60.3-32.2 18 20.1 44.3 33.1 73.8 33.1 29.6 0 55.8-13 73.8-33.1 18 20.1 44.3 33.1 73.8 33.1 29.6 0 55.8-13 73.8-33.1 15.3 17 36.3 28.9 60.3 32.2zm47.9-133c-3.2 6.8-10.9 18.6-27 20.8-2.4.3-4.8.5-7.2.5-14.7 0-28.2-6.1-38.1-17.2L455.7 151 420 190.8c-9.9 11.1-23.5 17.2-38.1 17.2s-28.2-6.1-38.1-17.2L308 151l-35.7 39.8c-9.9 11.1-23.5 17.2-38.1 17.2-14.7 0-28.2-6.1-38.1-17.2L160.3 151l-35.7 39.8c-9.9 11.1-23.5 17.2-38.1 17.2-2.5 0-4.9-.2-7.2-.5-16-2.2-23.8-13.9-27-20.8-5-10.8-7.1-27.6 2.3-42.6L114.8 48h386.3l60.2 96.1c9.5 15.1 7.5 31.9 2.4 42.7z"/></svg>

chevron-down.tpl

<svg class="{{ svg_custom_class }}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M441.9 167.3l-19.8-19.8c-4.7-4.7-12.3-4.7-17 0L224 328.2 42.9 147.5c-4.7-4.7-12.3-4.7-17 0L6.1 167.3c-4.7 4.7-4.7 12.3 0 17l209.4 209.4c4.7 4.7 12.3 4.7 17 0l209.4-209.4c4.7-4.7 4.7-12.3 0-17z"/></svg>

chevron-up.tpl

<svg class="{{ svg_custom_class }}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M6.101 359.293L25.9 379.092c4.686 4.686 12.284 4.686 16.971 0L224 198.393l181.13 180.698c4.686 4.686 12.284 4.686 16.971 0l19.799-19.799c4.686-4.686 4.686-12.284 0-16.971L232.485 132.908c-4.686-4.686-12.284-4.686-16.971 0L6.101 342.322c-4.687 4.687-4.687 12.285 0 16.971z"/></svg>

info-circle.tpl

<svg class="{{ svg_custom_class }}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M256 8C119.043 8 8 119.083 8 256c0 136.997 111.043 248 248 248s248-111.003 248-248C504 119.083 392.957 8 256 8zm0 448c-110.532 0-200-89.431-200-200 0-110.495 89.472-200 200-200 110.491 0 200 89.471 200 200 0 110.53-89.431 200-200 200zm0-338c23.196 0 42 18.804 42 42s-18.804 42-42 42-42-18.804-42-42 18.804-42 42-42zm56 254c0 6.627-5.373 12-12 12h-88c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h12v-64h-12c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h64c6.627 0 12 5.373 12 12v100h12c6.627 0 12 5.373 12 12v24z"/></svg>

truck.tpl

<svg class="{{ svg_custom_class }}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path d="M624 368h-16V251.9c0-19-7.7-37.5-21.1-50.9L503 117.1C489.6 103.7 471 96 452.1 96H416V56c0-30.9-25.1-56-56-56H56C25.1 0 0 25.1 0 56v304c0 30.9 25.1 56 56 56h8c0 53 43 96 96 96s96-43 96-96h128c0 53 43 96 96 96s96-43 96-96h48c8.8 0 16-7.2 16-16v-16c0-8.8-7.2-16-16-16zm-464 96c-26.5 0-48-21.5-48-48s21.5-48 48-48 48 21.5 48 48-21.5 48-48 48zm208-96H242.7c-16.6-28.6-47.2-48-82.7-48s-66.1 19.4-82.7 48H56c-4.4 0-8-3.6-8-8V56c0-4.4 3.6-8 8-8h304c4.4 0 8 3.6 8 8v312zm48-224h36.1c6.3 0 12.5 2.6 17 7l73 73H416v-80zm64 320c-26.5 0-48-21.5-48-48s21.5-48 48-48 48 21.5 48 48-21.5 48-48 48zm80-100.9c-17.2-25.9-46.6-43.1-80-43.1-24.7 0-47 9.6-64 24.9V272h144v91.1z"/></svg>

sync-alt.tpl 

<svg class="{{ svg_custom_class }}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M483.515 28.485L431.35 80.65C386.475 35.767 324.485 8 256 8 123.228 8 14.824 112.338 8.31 243.493 7.971 250.311 13.475 256 20.301 256h28.045c6.353 0 11.613-4.952 11.973-11.294C66.161 141.649 151.453 60 256 60c54.163 0 103.157 21.923 138.614 57.386l-54.128 54.129c-7.56 7.56-2.206 20.485 8.485 20.485H492c6.627 0 12-5.373 12-12V36.971c0-10.691-12.926-16.045-20.485-8.486zM491.699 256h-28.045c-6.353 0-11.613 4.952-11.973 11.294C445.839 370.351 360.547 452 256 452c-54.163 0-103.157-21.923-138.614-57.386l54.128-54.129c7.56-7.56 2.206-20.485-8.485-20.485H20c-6.627 0-12 5.373-12 12v143.029c0 10.691 12.926 16.045 20.485 8.485L80.65 431.35C125.525 476.233 187.516 504 256 504c132.773 0 241.176-104.338 247.69-235.493.339-6.818-5.165-12.507-11.991-12.507z"/></svg>

CSS

Requisito:

Ter adicionado helper classes em seu layout. Você pode seguir este pequeno tutorial para fazer isso (é só copiar e colar algumas classes, não leva mais que 1 minuto).

1. Adicionamos o seguinte SASS de cores em style-colors.scss.tpl (ou na stylesheet do seu layout que possui as cores e fontes da loja). Lembre-se de que as variáveis de cores e fontes podem variar em relação ao seu layout:

{# This mixin adds browser prefixes to a CSS property #}


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


{# /* // Buttons */ #}


.btn{
  text-decoration: none;
  text-align: center;
  border: 0;
  cursor: pointer;
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
  text-transform: uppercase;
  background: none;
  @include prefix(transition, all 0.4s ease, webkit ms moz o);
  &:hover,
  &:focus{
    outline: 0;
    opacity: 0.8;
  }
  &[disabled],
  &[disabled]:hover{
    opacity: 0.5;
    cursor: not-allowed;
    outline: 0;
  }
  &-default{
    padding: 10px 15px; 
    background-color: rgba($main-foreground, .2);
    color: $main-foreground;
    fill: $main-foreground;
    font-weight: bold;
  }
  &-primary{
    padding: 15px;
    background-color: $primary-color;
    color: $main-background;
    fill: $main-background;
    letter-spacing: 4px;
    @extend %body-font;
    &:hover{
      color: $main-background;
      fill: $main-background;
    }
  }
  &-secondary{
    padding: 10px 15px; 
    background-color: $main-background;
    color: $main-foreground;
    fill: $main-foreground;
    border: 1px solid $main-foreground;
  }
  &-block{
    float: left;
    width: 100%;
  }
}


{# /* // 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);
  }
}


{# /* // Forms */ #}


input,
textarea {
  font-family: $body-font;
}


.form-control {
  display: block;
  padding: 10px 8px;
  width: 100%;
  border: 0;
  border-bottom: 1px solid rgba($main-foreground, .5);
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
  color: $main-foreground;
  background-color: $main-background;
  &:focus{
    outline: 0;
  }
  &-inline{
    display: inline;
  }
}


.form-control::-webkit-input-placeholder { 
  color: $main-foreground;
}
.form-control:-moz-placeholder {
  color: $main-foreground;
}
.form-control::-moz-placeholder {
  color: $main-foreground;
}
.form-control:-ms-input-placeholder {
  color: $main-foreground;
}


.radio-button {
  input[type="radio"]{
    & +  .radio-button-content .unchecked{
      border:2px solid $main-foreground;
    }
    & +  .radio-button-content .checked{
      background-color: $main-foreground;
    }
  }
}

.spinner-ellipsis .point {
  background-color: rgba($main-foreground, 0.2);
}

2. Adicione os estilos no arquivo static/style-critical.tpl

Se em seu layout você usar um stylesheet para o CSS crítico, precisaremos do seguinte código dentro dele, do contrário, você pode unificar o CSS dos passos 2 e 3 em um único arquivo.

{# /* // Placeholders and preloaders */ #}

.spinner-ellipsis {
  position: relative;
  display: inline-block;
  width: 64px;
  height: 40px;
}
.spinner-ellipsis .point {
  position: absolute;
  top: 15px;
  width: 11px;
  height: 11px;
  border-radius: 50%;
  animation-timing-function: cubic-bezier(0, 1, 1, 0);
}
.spinner-ellipsis .point:nth-child(1) {
  left: 6px;
  animation: spinner-ellipsis1 0.6s infinite;
}
.spinner-ellipsis .point:nth-child(2) {
  left: 6px;
  animation: spinner-ellipsis2 0.6s infinite;
}
.spinner-ellipsis .point:nth-child(3) {
  left: 26px;
  animation: spinner-ellipsis2 0.6s infinite;
}
.spinner-ellipsis .point:nth-child(4) {
  left: 45px;
  animation: spinner-ellipsis3 0.6s infinite;
}
@keyframes spinner-ellipsis1 {
  0% {
    transform: scale(0);
  }
  100% {
    transform: scale(1);
  }
}
@keyframes spinner-ellipsis3 {
  0% {
    transform: scale(1);
  }
  100% {
    transform: scale(0);
  }
}
@keyframes spinner-ellipsis2 {
  0% {
    transform: translate(0, 0);
  }
  100% {
    transform: translate(19px, 0);
  }
}


{# /* // Animations */ #}


.transition-up {
  opacity: 0;
}

{# /* // Forms */ #}


.form-group {
  position: relative;
  width: 100%;
}
.form-group .form-select-icon{
  position: absolute;
  bottom: 12px;
  right: 0;
  pointer-events: none;
}
.form-row {
  width: auto;
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
  -ms-flex-wrap: wrap;
  flex-wrap: wrap;
  margin-right: -5px;
  margin-left: -5px;
  clear: both;
}


.form-row > .col,
.form-row > [class*=col-]{
  padding-right: 5px;
  padding-left: 5px;
}


.form-label {
  display: block;
  font-size: 10px;
  text-transform: uppercase;
}

/*============================================================================
  #Cart detail
==============================================================================*/


{# /* // Shipping Calculator */ #}


.free-shipping-title {
  position: relative;
  width: 100%;
  height: 55px;
}
.shipping-calculator-head.with-zip {
  height: 65px;
}
.shipping-calculator-head.with-zip.with-free-shipping {
  height: 110px;
}
.shipping-calculator-head.with-form {
  height: 110px;
}
.shipping-calculator-head.with-form + .shipping-spinner-container {
  margin-top: -20px;
}
.shipping-calculator-head.with-error {
  height: 155px;
}

3. Adicione os estilos no arquivo static/style-async.tpl

Se em seu layout você usar um stylesheet CSS assíncrono, precisaremos do seguinte código dentro dela, do contrário, você pode unificar o CSS dos passos 2 e 3 em um único arquivo.

{# /* // Margin and Padding */ #}


%section-margin {
  margin-bottom: 70px;
}
%element-margin {
  margin-bottom: 35px;
}
%element-margin-small {
  margin-bottom: 20px;
}


{# /* // Mixins */ #}


{# This mixin adds browser prefixes to a CSS property #}


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

{# /* // Animations */ #}

.transition-up {
  position: relative;
  top: -8px;
  z-index: 10;
  @include prefix(transition, all 0.5s ease, webkit ms moz o);
  pointer-events: none; 
  &-active {
    top: 0;
    opacity: 1; 
    z-index: 100;
    pointer-events: all; 
  }
}

{# /* // Forms */ #}


.form-group{
  @extend %element-margin;
  .form-label{
    float: left;
    width: 100%;
    margin-bottom: 10px;
  }
  .alert{
    margin: 10px 0 0 0;
  }
}


.radio-button{
  width: 100%;
  float: left;
  clear: both;
  text-align: left;
  -webkit-tap-highlight-color: rgba(0,0,0,0);
  @extend %element-margin-small;
  cursor: pointer;
  &.disabled{
    opacity: 0.6;
    cursor: not-allowed;
    input[type="radio"] {
      cursor: not-allowed;
    }
  }
  &-icons{
    position: relative;
    float: left;
    display: table;
    width: 20px;
    margin:0 5px 0 0;
  }
  &-icon{
    border-radius: 50%;
    width: 18px;
    height: 18px;
  }
  input[type="radio"]{
    display: none;
    & +  .radio-button-content .unchecked{
      float: left;
    }
    & +  .radio-button-content .checked{
      position: absolute;
      left:9px;
      top:9px;
      width:0;
      height: 0;      
      @include prefix(transform, translate(-50%,-50%), webkit ms moz o);
      @include prefix(transition, all 0.2s , webkit ms moz o);
    }
    &:checked + .radio-button-content .checked{
      width: 6px;
      height: 6px;
    }
  }
  &-label{
    display: table;
    padding-top: 1px;
  }
  &-text{
    display: table;
    margin-bottom: 2px;
  }
}


.radio-button-item:last-of-type .radio-button{
  margin-bottom: 0;
}


.form-select {
  display: block;
  width: 100%;
  &:focus{
    outline:0;
  }
  &::-ms-expand {
    display: none;
  }
}


{# /* Disabled controls */ #}


input,
select,
textarea{
  &[disabled],
  &[disabled]:hover,
  &[readonly],
  &[readonly]:hover{
    background-color: #DDD;
    cursor: not-allowed; 
  }
}

JS

⚠️ A partir do dia 30 de janeiro de 2023, a biblioteca jQuery será removida do código de nossas lojas, portanto, a função "$" não poderá ser utilizada.

1. JavaScript precisam ser adicionados no arquivo store.js.tpl (ou onde você tem suas funções JS). O código que precisamos é o seguinte:

{#/*============================================================================
      #Shipping calculator
    ==============================================================================*/ #}

    window.urls = {
     "shippingUrl": "{{ store.shipping_calculator_url | escape('js') }}"
    }

    {# /* // Select and save shipping function */ #}

    selectShippingOption = function(elem, save_option) {
        jQueryNuvem(".js-shipping-method, .js-branch-method").removeClass('js-selected-shipping-method');
        jQueryNuvem(elem).addClass('js-selected-shipping-method');
        if (save_option) {
            LS.saveCalculatedShipping(true);
        }
        if(jQueryNuvem(elem).hasClass("js-shipping-method-hidden")){


            {# Toggle other options visibility depending if they are pickup or delivery for cart and product at the same time #}


            if(jQueryNuvem(elem).hasClass("js-pickup-option")){
                jQueryNuvem(".js-other-pickup-options, .js-show-other-pickup-options .js-shipping-see-less").show();
                jQueryNuvem(".js-show-other-pickup-options .js-shipping-see-more").hide();


            }else{
                jQueryNuvem(".js-other-shipping-options, .js-show-more-shipping-options .js-shipping-see-less").show();
                jQueryNuvem(".js-show-more-shipping-options .js-shipping-see-more").hide()
            }          
        }
    };

   {# Apply zipcode saved by cookie if there is no zipcode saved on cart from backend #}

    if (cookieService.get('calculator_zipcode')) {

        {# If there is a cookie saved based on previous calcualtion, add it to the shipping input to triggert automatic calculation #}

        var zipcode_from_cookie = cookieService.get('calculator_zipcode');
        
        {% if settings.ajax_cart %}

            {# If ajax cart is active, target only product input to avoid extra calulation on empty cart #}

            jQueryNuvem('#product-shipping-container .js-shipping-input').val(zipcode_from_cookie);

        {% else %}

            {# If ajax cart is inactive, target the only input present on screen #}

            jQueryNuvem('.js-shipping-input').val(zipcode_from_cookie);

        {% endif %}

        jQueryNuvem(".js-shipping-calculator-current-zip").text(zipcode_from_cookie);

        {# Hide the shipping calculator and show spinner  #}

        jQueryNuvem(".js-shipping-calculator-head").addClass("with-zip").removeClass("with-form");
        jQueryNuvem(".js-shipping-calculator-with-zipcode").addClass("transition-up-active");
        jQueryNuvem(".js-shipping-calculator-spinner").show();
    } else {

        {# If there is no cookie saved, show calcualtor #}

        jQueryNuvem(".js-shipping-calculator-form").addClass("transition-up-active");
    }           

    {# Remove shipping suboptions from DOM to avoid duplicated modals #}

    removeShippingSuboptions = function(){
        var shipping_suboptions_id = jQueryNuvem(".js-modal-shipping-suboptions").attr("id");
        jQueryNuvem("#" + shipping_suboptions_id).remove();
        jQueryNuvem('.js-modal-overlay[data-modal-id="#' + shipping_suboptions_id + '"').remove();
    };

    {# /* // Calculate shipping function */ #}

    jQueryNuvem(".js-calculate-shipping").on("click", function (e) {
        e.preventDefault();

        {# Take the Zip code to all shipping calculators on screen #}
        let shipping_input_val = jQueryNuvem(e.currentTarget).closest(".js-shipping-calculator-form").find(".js-shipping-input").val();

        jQueryNuvem(".js-shipping-input").val(shipping_input_val);

        {# Calculate on page load for both calculators: Product and Cart #}

        {% if template == 'product' %}
             if (!vanillaJS) {
                LS.calculateShippingAjax(
                    jQueryNuvem('#product-shipping-container').find(".js-shipping-input").val(),
                    '{{store.shipping_calculator_url | escape('js')}}',
                    jQueryNuvem("#product-shipping-container").closest(".js-shipping-calculator-container") );
             }
        {% endif %}

        if (jQueryNuvem(".js-cart-item").length) {
            LS.calculateShippingAjax(
            jQueryNuvem('#cart-shipping-container').find(".js-shipping-input").val(),
            '{{store.shipping_calculator_url | escape('js')}}',
            jQueryNuvem("#cart-shipping-container").closest(".js-shipping-calculator-container") );
        }

        jQueryNuvem(".js-shipping-calculator-current-zip").html(shipping_input_val);
        removeShippingSuboptions();


    });

    {# /* // Calculate shipping by submit */ #}

    jQueryNuvem(".js-shipping-input").on('keydown', function (e) {
        var key = e.which ? e.which : e.keyCode;
        var enterKey = 13;
        if (key === enterKey) {
            e.preventDefault();
            jQueryNuvem(e.currentTarget).closest(".js-shipping-calculator-form").find(".js-calculate-shipping").trigger('click');
            if (window.innerWidth < 768) {
                jQueryNuvem(e.currentTarget).trigger('blur');
            }
        }
    });

    {# /* // Shipping and branch click */ #}

    jQueryNuvem(document).on("change", ".js-shipping-method, .js-branch-method", function (e) {
        selectShippingOption(this, true);
        jQueryNuvem(".js-shipping-method-unavailable").hide();
    });

    {# /* // Select shipping first option on results */ #}

    jQueryNuvem(document).on('shipping.options.checked', '.js-shipping-method', function (e) {
        let shippingPrice = jQueryNuvem(this).attr("data-price");
        LS.addToTotal(shippingPrice);


        let total = (LS.data.cart.total / 100) + parseFloat(shippingPrice);
        jQueryNuvem(".js-cart-widget-total").html(LS.formatToCurrency(total));


        selectShippingOption(this, false);
    });

    {# /* // Toggle branches link */ #}

    jQueryNuvem(document).on("click", ".js-toggle-branches", function (e) {
        e.preventDefault();
        jQueryNuvem(".js-store-branches-container").slideToggle("fast");
        jQueryNuvem(".js-see-branches, .js-hide-branches").toggle();
    });

    {# /* // Toggle more shipping options */ #}

    jQueryNuvem(document).on("click", ".js-toggle-more-shipping-options", function(e) {
        e.preventDefault();

        {# Toggle other options depending if they are pickup or delivery for cart and product at the same time #}

        if(jQueryNuvem(this).hasClass("js-show-other-pickup-options")){
            jQueryNuvem(".js-other-pickup-options").slideToggle(600);
            jQueryNuvem(".js-show-other-pickup-options .js-shipping-see-less, .js-show-other-pickup-options .js-shipping-see-more").toggle();
        }else{
            jQueryNuvem(".js-other-shipping-options").slideToggle(600);
            jQueryNuvem(".js-show-more-shipping-options .js-shipping-see-less, .js-show-more-shipping-options .js-shipping-see-more").toggle();
        }
    });

    {# /* // Calculate shipping on page load */ #}

    {# Only shipping input has value, cart has saved shipping and there is no branch selected #}

    calculateCartShippingOnLoad = function(){
        if(jQueryNuvem("#cart-shipping-container .js-shipping-input").val()){
       
            // If user already had calculated shipping: recalculate shipping


            setTimeout(function() { 
                LS.calculateShippingAjax(
                    jQueryNuvem('#cart-shipping-container').find(".js-shipping-input").val(),
                    '{{store.shipping_calculator_url | escape('js')}}',
                    jQueryNuvem("#cart-shipping-container").closest(".js-shipping-calculator-container") );
                    removeShippingSuboptions();
            }, 100);
        }

        if(jQueryNuvem(".js-branch-method").hasClass('js-selected-shipping-method')){
            {% if store.branches|length > 1 %}
                jQueryNuvem(".js-store-branches-container").slideDown("fast");
                jQueryNuvem(".js-see-branches").hide();
                jQueryNuvem(".js-hide-branches").show();
            {% endif %}
        }
    };
    
    {% if cart.has_shippable_products %}
        calculateCartShippingOnLoad();
    {% endif %}

    {# /* // Calculate product detail shipping on page load */ #}

    {% if template == 'product' %}

        if (!vanillaJS) {
            if(jQueryNuvem('#product-shipping-container').find(".js-shipping-input").val()){
                setTimeout(function() {
                    LS.calculateShippingAjax(
                        jQueryNuvem('#product-shipping-container').find(".js-shipping-input").val(),
                        '{{store.shipping_calculator_url | escape('js')}}',
                        jQueryNuvem("#product-shipping-container").closest(".js-shipping-calculator-container") );
                    removeShippingSuboptions();
                }, 100);
            }
        }

    {% endif %}

    {# /* // Change CP */ #}

    jQueryNuvem(document).on("click", ".js-shipping-calculator-change-zipcode", function(e) {
        e.preventDefault();
        jQueryNuvem(".js-shipping-calculator-response").fadeOut(100);
        jQueryNuvem(".js-shipping-calculator-head").addClass("with-form").removeClass("with-zip");
        jQueryNuvem(".js-shipping-calculator-with-zipcode").removeClass("transition-up-active");
        jQueryNuvem(".js-shipping-calculator-form").addClass("transition-up-active");
    }); 

    {# /* // Shipping provinces */ #}

    {% if provinces_json %}
        jQueryNuvem('select[name="country"]').on("change", function (e) {
            var provinces = {{ provinces_json | default('{}') | raw }};
            LS.swapProvinces(provinces[jQueryNuvem(e.currentTarget).val()]);
        }).trigger('change');
    {% endif %}

    {# /* // Change store country: From invalid zipcode message */ #}
    
    changeLang = function(element) {
      var selected_country_url = element.find("option").filter((el) => el.selected).attr("data-country-url");
      location.href = selected_country_url;
    };

   jQueryNuvem('.js-lang-select').on("change", function (e) {
      lang_select_option = jQueryNuvem(this);

      changeLang(lang_select_option);
    });

    jQueryNuvem(document).on("click", ".js-save-shipping-country", function(e) {

        e.preventDefault();

        {# Change shipping country #}

        lang_select_option = jQueryNuvem(this).closest(".js-modal-shipping-country");
        changeLang(lang_select_option);


        jQueryNuvem(this).text('{{ "Aplicando..." | translate }}').addClass("disabled");
    });

2. Dentro da função para alterar variantes, alteramos a função changeVariant antes de fechar, adicionamos o seguinte:

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);

3. Se o seu layout tem um carrinho de compras rápido, precisamos adicionar o seguinte código dentro do callback quando um produto for adicionado com sucesso. Isso pode ser encontrado na função acionada pelo botão js-addtocart. Localizado aqui, adicionamos o seguinte:

{# Update shipping input zipcode on add to cart #}

{# Use zipcode from input if user is in product page, or use zipcode cookie if is not #}

if (jQueryNuvem("#product-shipping-container .js-shipping-input").val()) {
    zipcode_on_addtocart = jQueryNuvem("#product-shipping-container .js-shipping-input").val();
    jQueryNuvem("#cart-shipping-container .js-shipping-input").val(zipcode_on_addtocart);
    jQueryNuvem(".js-shipping-calculator-current-zip").text(zipcode_on_addtocart);
} else if (cookieService.get('calculator_zipcode')){
    var zipcode_from_cookie = cookieService.get('calculator_zipcode');
    jQueryNuvem('.js-shipping-input').val(zipcode_from_cookie);
    jQueryNuvem(".js-shipping-calculator-current-zip").text(zipcode_from_cookie);
}

Configurações

No arquivo config/settings.txt, adicionaremos um checkbox para ativar ou desativar a calculadora e as lojas físicas na seção Personalizar meu layout atual no Administrador nuvem

Na seção Detalhe do produto, adicionamos o seguinte:

    title
        title = Medios de envío
    checkbox_global
        name = shipping_calculator_product_page
        description = Mostrar calculador de costos de envío en la página de producto

E fazemos o mesmo para a seção de Carrinho de compras

    title
        title = Medios de envío
    checkbox_global
        name = shipping_calculator_cart_page
        description = Mostrar calculador de costos de envío en el carrito

Traduções

Nesta etapa adicionamos os textos para as traduções no arquivo config/translations.txt

es "Formas de envío"
pt "Formas de envio"
en "Shipping methods"
es_mx "Opciones de envío"

es "FORMAS DE ENVÍO"
pt "FORMAS DE ENVIO"
en "SHIPPING METHODS"
es_mx "OPCIONES DE ENVÍO"

es "Ejemplos de formas de envío"
pt "Exemplos de formas de envio"
en "Shipping methods examples"
es_mx "Ejemplos de opciones de envío"

es "Ingrese aquí su código postal para calcular su costo de envío"
pt "Digite aqui o seu CEP para calcular o frete"
en "Enter your zipcode to calculate your shipping cost"
es_mx "Ingresa tu código postal para calcular el costo de envío"

es "Calculá el costo de tu envío"
pt "Cálculo de frete"
en "Enter your zipcode to calculate your shipping cost"
es_mx "Calcula el costo de tu envío"

es "Calcular envío"
pt "Calcular frete"
en "Calculate shipping"
es_mx "Calcular envío"

es "Calcular"
pt "Calcular"
en "Calculate"
es_mx "Calcular"

es "Código postal"
pt "CEP"
en "Zipcode"
es_mx "Código postal"

es "Tu código postal"
pt "Seu CEP"
en "Your zipcode"
es_mx "Tu código postal"


es "No sé mi código"
pt "Não sei meu CEP"
en "I don't know my code"
es_mx "No sé mi código"

es "Calcular costo de envío"
pt "Calcular Frete"
en "Calculate shipping"
es_mx "Calcular costo de envío"

es "El código postal es inválido."
pt "O CEP está inválido."
en "The zipcode is not valid."
es_mx "El código postal es inválido."

es " (sin envío)"
pt " (sem frete)"
en " (without shipping cost)"
es_mx " (sin envío)"

es "El código postal es inválido. Por favor intentá de nuevo usando otro."
pt "CEP inválido. Por favor, digite novamente ou informe outro."
en "The zipcode is not valid. Please try again using another."
es_mx "El código postal es inválido. Intenta de nuevo ingresando otro."

es "Ocurrió un error al calcular el envío. Por favor intentá de nuevo en unos segundos."
pt "Erro no cálculo. Por favor, tente novamente em alguns segundos."
en "An error ocurred while calculating the shipping. Please try again in a few seconds."
es_mx "Hubo un error al calcular el envío. Intenta de nuevo en unos segundos."

es "El calculo falló por un problema con el medio de envío. Por favor intentá de nuevo en unos segundos."
pt "Erro no meio de envio. Por favor, tente novamente em alguns segundos."
en "The calculation failed due to a problem with the shipping method. Please try again in a few seconds."
es_mx "El cálculo falló por un problema con el forma de envío. Intenta de nuevo en unos segundos."

es "Vea las opciones de envío para su código postal abajo"
pt "Veja os valores para o seu CEP abaixo"
en "Your shipping options are the following"
es_mx "Conoce las opciones de entrega para tu código postal"

es "No hay costos de envío para el código postal dado."
pt "Não há frete disponível para o CEP informado"
en "Shipping is not available for your zipcode."
es_mx "No hay costos de envío para ese código postal."

es "Retirá gratis en nuestros locales"
pt "Retire grátis em nossas lojas físicas"
en "Free pickup on our stores"
es_mx "Recoge gratis en nuestras tiendas"

es "Retirá gratis en nuestro local"
pt "Retire grátis em nossa loja física"
en "Free pickup on our store"
es_mx "Recoge gratis en nuestra tienda"

es "Nuestros locales"
pt "Nossas lojas"
en "Our stores"
es_mx "Nuestras tiendas"

es "Nuestro local"
pt "Nossa loja"
en "Our store"
es_mx "Nuestra tienda"

es "Ver opciones"
pt "Ver opções"
en "See options"
es_mx "Ver opciones"

es "Ocultar opciones"
pt "Esconder opções"
en "Hide options"
es_mx "Ocultar opciones"

es "Ver locales"
pt "Ver lojas físicas"
en "See our stores"
es_mx "Ver tiendas"

es "Ver local"
pt "Ver loja física"
en "See our store"
es_mx "Ver tienda"

es "Elegir local"
pt "Escolher loja física"
en "Select store"
es_mx "Elegir una tienda"

es "Nuestros locales"
pt "Nossas lojas"
en "Our stores"
es_mx "Nuestras tiendas"

es "Nuestro local"
pt "Nossa loja"
en "Our store"
es_mx "Nuestra tienda"

es "Ver opciones"
pt "Ver opções"
en "See options"
es_mx "Ver opciones"

es "Ocultar opciones"
pt "Esconder opções"
en "Hide options"
es_mx "Ocultar opciones"

es "Conocé nuestras opciones de envío"
pt "Conheça nossas opções de frete"
en "See our shipping options"
es_mx "Conoce nuestras opciones de entrega"

es "Elegí nuestras opciones de envío"
pt "Escolha a opção de frete"
en "Choose our shipping options"
es_mx "Conoce las opciones de entrega"

es "Calculando"
pt "Calculando"
en "Calculating"
es_mx "Calculando"

es "El medio de envío que habías elegido ya no se encuentra disponible "
pt "O frete escolhido não está mais disponível "
en "The shipping method selected is no longer available "
es_mx "La forma de envío que elegiste ya no está disponible "

es "porque el total de los items del carrito superan el peso máximo."
pt "porque o total de itens no carrinho excede o peso máximo."
en "because the total of the items in the cart surpass the maximum weight."
es_mx "porque el peso total del carrito supera el peso máximo."

es "¡No te preocupes! Podés elegir otro medio de envío."
pt "Mas não se preocupe! Você pode escolher outro meio de envio."
en "Do not worry! You can select another method."
es_mx "¡No te preocupes! Puedes elegir otra forma de envío."

es "Ocultar locales"
pt "Esconder lojas físicas"
en "Hide our stores"
es_mx "Ocultar tiendas"

es "Ocultar local"
pt "Esconder loja física"
en "Hide our store"
es_mx "Ocultar tienda"

es "Seguí acá"
pt "Acompanhe aqui"
en "Track here"
es_mx "Sigue aquí"

es "tu última compra"
pt "seu último pedido"
en "your last order"
es_mx "tu última compra"

es "El precio de envío incluye este producto y todos los que agregaste al carrito."
pt "O custo de frete inclui este produto e outros adicionados ao carrinho."
en "The shipping cost includes this product and all the products added to the cart."
es_mx "El precio de envío incluye este producto y todos los que has agregado al carrito."

es "Vas a poder elegir alguna de las siguientes opciones antes de finalizar la compra:"
pt "Você pode escolher uma das seguintes opções antes de finalizar a compra"
en "You will be able to choose some of the following options before ending the purchase"
es_mx "Podrás elegir alguna de las siguientes opciones antes de finalizar la compra:"

es "No encontramos este código postal. ¿Está bien escrito?"
pt "Não conseguimos encontrar esse CEP. Está bem escrito?"
en "We couldn&#39;t find this zipcode. Is it written right?"
es_mx "No encontramos este código postal. ¿Está bien escrito?"

es "El tiempo de entrega no considera feriados."
pt "O prazo de entrega não contabiliza feriados."
en "The delivery time does not consider holidays."
es_mx "El tiempo de entrega no considera feriados."

es "Ingresa aquí tu código postal para calcular tu costo de envío"
pt "Digite aqui o seu CEP para calcular o frete"
en "Enter your zipcode to calculate your shipping cost"
es_mx "Ingresa aquí tu código postal para calcular tu costo de envío"

es "Costos de envío"
pt "Custos de frete"
en "Shipping costs"
es_mx "Costos de envío"

es "Calcular"
pt "Calcular"
en "Calculate"
es_mx "Calcular"

es "Envío:"
pt "Frete:"
en "Shipping:"
es_mx "Envío:"

es "Calcular para ver"
pt "Calcule para visualizar"
en "Calculate to view"
es_mx "Calcular para ver"

es "El costo del envío se calculará después."
pt "O frete será calculado depois."
en "Shipping cost will be calculated at checkout"
es_mx "El costo del envío se calculará después."

es "<strong>Tenés envío gratis</strong> en esta compra. ¡Aprovechalo!"
pt "Você ganhou <strong>frete grátis</strong> para esta compra. Aproveite!"
en "You have <strong>free shipping</strong> in this order. Go for it!"
es_mx "<strong>Tienes envío gratis</strong> en esta compra. ¡Aprovéchalo!"

es "¡Genial! <strong class='text-primary'>Tenés envío gratis</strong>"
pt "Sucesso! Você <strong class='text-primary'>tem frete grátis</strong>"
en "You have <strong class='text-primary'>free shipping</strong>"
es_mx "¡Genial! <strong class='text-primary'>Tienes envío gratis</strong>"

es "¡Estas a <strong class='js-cart-free-shipping-amount'></strong> de tener <strong class='text-primary m-quarter-bottom'>envío gratis</strong> en tu compra!"
pt "<strong>Ganhe frete grátis</strong> com mais <strong class='js-cart-free-shipping-amount'></strong> em compras"
en "You need <strong class='js-cart-free-shipping-amount'></strong> to have <strong class='text-primary m-quarter-bottom'>free shipping</strong> on your order"
es_mx "¡Estas a <strong class='js-cart-free-shipping-amount'></strong> de tener <strong class='text-primary m-quarter-bottom'>envío gratis</strong> en tu compra!"

es "Ver más productos"
pt "Escolher mais produtos"
en "See more products"
es_mx "Ver más productos"

es "¡Todos los productos de esta categoría <strong>tienen envío gratis!</strong>"
pt "Todos os produtos desta categoria <strong>tienen envío gratis!</strong>"
en "All the products in this category <strong>have free shipping!</strong>"
es_mx "¡Todos los productos de esta categoría <strong>tienen envío gratis!</strong>"

es "<strong class='text-primary'>Envío gratis</strong> superando los"
pt "<strong class='text-primary'>Frete grátis</strong> a partir de"
en "<strong class='text-primary'>Free shipping</strong> buying more than"
es_mx "<strong class='text-primary'>Envío gratis</strong> superando los"

es "<strong class='text-primary'>¡Envío gratis!</strong> con todos los medios de envío"
pt "<strong class='text-primary'>Frete grátis!</strong> com todos os meios de envio"
en "<strong class='text-primary'>Free shipping!</strong> with all shipping options"
es_mx "<strong class='text-primary'>¡Envío gratis!</strong> con todos los medios de envío"
es "Medios de envío"
pt "Meios de envio"
en "Shipping Methods"
es_mx "Opciones de envío"

es "Mostrar calculador de costos de envío en la página de producto"
pt "Mostrar o Cálculo de Frete na página de produto"
en "Show shipping cost calculator on product page"
es_mx "Mostrar el calculador de costos de envío en la página de producto"

es "Mostrar calculador de costos de envío en el carrito"
pt "Mostrar o Cálculo de Frete no carrinho"
en "Show shipping cost calculator to cart"
es_mx "Mostrar el calculador de costos de envío en el carrito"

es "Mostrar medios de envío en tu tienda"
pt "Mostrar as opções de frete na sua loja"
en "Show Shipping Methods in your store"
es_mx "Mostrar las opciones de envío en tu sitio"

es "Mostrar medios de pago en tu tienda"
pt "Mostrar as opções de pagamento na sua loja"
en "Show payment methods in your store"
es_mx "Mostrar los métodos de pago en tu sitio"

es "Ver puntos de retiro"
pt "Ver pontos de retirada"
en "See pickup points"
es_mx "Ver puntos de retiro"

es "Puntos de retiro"
pt "Pontos de retirada"
en "Pickup points"
es_mx "Puntos de retiro"

es "Cercanos al CP:"
pt "Próximos ao CEP:"
en "Near zipcode:"
es_mx "Cercanos al CP:"

es "Vas a poder elegir estas opciones antes de finalizar tu compra"
pt "Você poderá escolher essas opções antes de finalizar sua compra"
en "You will be able to select this options before you finish your purchase"
es_mx "Podrás elegir estas opciones antes de finalizar tu compra"

es "Entregas para el CP:"
pt "Entregas para o CEP:"
en "Shipping for zipcode:"
es_mx "Entregas para el CP:"

es "Cambiar CP"
pt "Alterar CEP"
en "Change zipcode"
es_mx "Cambiar CP"

Ativação

Você pode ativar a calculadora n detalhe do produto e no carrinho através do Administrador Nuvem, na seção Personalizar meu  layout atual nas seções Detalhes do produto e Carrinho de compras:


Pronto, você já tem em seu layout a funcionalidade aplicada.