Cálculo de envíos internacionales

En este tutorial vamos a ver cómo agregar la posibilidad de calcular envío para otros países sin necesidad de cambiar de moneda o idioma.

Vale la pena explicar que en las tiendas existen dos tipos de entidades por cada país configurado desde la pantalla de Configuraciones > Idiomas y Monedas. 

  • storefront_country: Es el país que define el idioma y la moneda. 
  • shipping_country: Es el país al cual se envía la compra (se puede elegir desde un desplegable en el paso 1 del checkout)

En todas las tiendas storefront_country es igual a shipping_country, por ende si el usuario cambia de idioma en una tienda, por ejemplo a Brasil, el país de envío también será Brasil.

En los siguientes pasos vamos a ver como hacer que el shipping_country sea independiente del storefront_country, por ende el usuario va a poder comprar mirando la tienda en su versión Argentina pero enviando a otro país sin necesidad de cambiar el storefront_country.

HTML

1. Lo primero que vamos a hacer es agregar el siguiente código para el texto que dice “Medios de envío para [País de envío]” en el snipplet shipping-calculator.tpl dentro de la carpeta snipplets/shipping. Debemos reemplazar el el texto que dice “Medios de envío”  por lo siguiente: 

{% if languages | length > 1 %}
    {{ ' para ' | translate }}
    {% for language in languages %}
        {% if (shipping_country_id and language.id == shipping_country_id) or (not shipping_country_id and language.active) %}
            <a href="#" data-toggle="#{% if product_detail %}product{% else %}cart{% endif %}-shipping-country" class="js-modal-open js-shipping-country-label btn-link btn-link-primary text-capitalize">
                {{ language.country_name }}
            </a>
        {% endif %}
    {% endfor %}
{% endif %}

Y luego al final de este archivo vamos a agregar el modal que permite el cambio de país:

{# 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.id }}" data-country-url="{{ language.url }}" {% if (shipping_country_id and language.id == shipping_country_id) or (not shipping_country_id and language.active) %}selected{% endif %}>{{ language.country_name }}</option>
                    {% endfor %}
                {% endblock select_options%}
            {% endembed %}
        {% endblock %}
        {% block modal_foot %}
            <a href="#" class="js-save-shipping-country js-modal-close btn btn-primary float-right">{{ 'Aplicar' | translate }}</a>
        {% endblock %}
    {% endembed %}
{% endif %}

2. En caso que no tengamos un componente modal.tpl para los pop ups, vamos a necesitar crearlo dentro de la carpeta snipplets, usando el siguiente código:

{# /*============================================================================
  #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 {% if modal_mobile_full_screen %}js-fullscreen-modal{% endif %} 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 }}" {% if modal_form_hook %}data-store="{{ modal_form_hook }}"{% endif %}>
    {% endif %}
    <div class="js-modal-close {% if modal_mobile_full_screen %}js-fullscreen-modal-close{% endif %} 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>

3. En caso de no tener un snipplet form-select.tpl para el componente select, vamos a tener que crearlo dentro de la carpeta snipplets/forms ya que lo usamos en shipping-calculator.tpl durante el paso 1. Este snipplet debe tener el siguiente código:

{# /*============================================================================
  #Form select
==============================================================================*/
 
#Properties
 
#Group
    //select_group_custom_class for custom CSS classes
#Label 
    // select_label_name for name
    // select_label_id for ID
    // select_for for label for
    // select_label_custom_class for custom CSS classes
#Select 
    // select_id for id
    // select_name for name
    // select_custom_class for custom CSS classes 
    // input_rows for textarea rows
    // select_options to insert select options
    // select_aria_label for aria-label attribute
 
#}
 
<div class="form-group {{ select_group_custom_class }}">
    {% if select_label %}
        <label {% if select_label_id%}id="{{ select_label_id }}"{% endif %} class="form-label {{ select_label_custom_class }}" {% if select_for %}for="{{ select_for }}"{% endif %}>{{ select_label_name }}</label>
    {% endif %}
    <select 
        {% if select_id %}id="{{ select_id }}"{% endif %}
        class="form-select {{ select_custom_class }} {% if select_inline %}form-control-inline{% endif %}"
        {% if select_data %}data-{{select_data}}="{{select_data_value}}"{% endif %}
        {% if select_name %}name="{{ select_name }}"{% endif %}
        {% if select_aria_label %}aria-label="{{ select_aria_label }}"{% endif %}>
        {% block select_options %}
        {% endblock select_options %}
    </select>
    <div class="form-select-icon">
        {% include "snipplets/svg/chevron-down.tpl" with {svg_custom_class: "icon-inline icon-w-14 icon-lg svg-icon-text"} %}
    </div>
</div>

4. Por último para la parte de HTML, dentro de la carpeta snipplets/SVG vamos sumar los SVGs que usamos para el select, el icono de filtrado y el modal.

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>

chevron-left.tpl

<svg class="{{ svg_custom_class }}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 512"><path d="M231.293 473.899l19.799-19.799c4.686-4.686 4.686-12.284 0-16.971L70.393 256 251.092 74.87c4.686-4.686 4.686-12.284 0-16.971L231.293 38.1c-4.686-4.686-12.284-4.686-16.971 0L4.908 247.515c-4.686 4.686-4.686 12.284 0 16.971L214.322 473.9c4.687 4.686 12.285 4.686 16.971-.001z"/></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>


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:

{# /* // Mixins */ #}
{# This mixin adds browser prefixes to a CSS property #}
@mixin prefix($property, $value, $prefixes: ()) {
  @each $prefix in $prefixes {
      #{'-' + $prefix + '-' + $property}: $value;
  }
    #{$property}: $value;
}
 
{# /* // Links */ #}
 
a {
  color: $main-foreground;
  fill: $main-foreground;
  @include prefix(transition, all 0.4s ease, webkit ms moz o);
  &:hover,
  &:focus{
    color: rgba($main-foreground, .5);
    fill: rgba($main-foreground, .5);
  }
}
.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);
  }
}
 
{# /* // Modals */ #}
 
.modal{
  color: $main-foreground;
  background-color:$main-background;
}
 
{# /* // Forms */ #}
 
input,
textarea {
  font-family: $body-font;
}
 
.form-control {
  display: block;
  padding: 8px;
  width: 100%;
  font-size: 16px; /* Hack to avoid autozoom on IOS */
  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;
}
 
.form-select{
  display: block;
  padding: 10px 0;
  width: 100%;
  font-size: 16px; /* Hack to avoid autozoom on IOS */
  border: 0;
  border-bottom: 1px solid rgba($main-foreground, .5);
  border-radius: 0;
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
  color: $main-foreground;
  background-color: $main-background;
  @extend %body-font;
  &-icon{
    background: $main-background;
  }
}

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 agregar el siguiente código debajo dentro de la misma, pero si no es el caso podés unificar el CSS de los pasos 2 y 3 en un solo archivo.

{# /* // Forms */ #}
 
.form-group {
  position: relative;
  width: 100%;
}
.form-group .form-select-icon{
  position: absolute;
  bottom: 12px;
  right: 0;
  pointer-events: none;
}

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 agregar el siguiente código dentro de la misma, pero si no es el caso podés unificar el CSS de los pasos 2 y 3 en un solo archivo.

{# /* // Mixins */ #}
 
{# This mixin adds browser prefixes to a CSS property #}
 
@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 0;
    clear: both;
  }
  &-full {
    width: 100%;
  }
  &-docked-md{
    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%;
    &-small{
      left: 50%;
      width: 80%;
      height: auto;
      @include prefix(transform, translate(-50%, -50%), webkit ms moz o);
      .modal-body{
        min-height: 150px;
        max-height: 400px;
        overflow: auto;
      }
    }
  }
  &-top.modal-show,
  &-bottom.modal-show {
    top: 0;
    &.modal-centered-small{
      top: 50%;
    }
  }
  &-bottom-sheet {
    top: initial;
    bottom: -100%;
    height: auto;
    &.modal-show {
      top: initial;
      bottom: 0;
      height: auto;
    }
  }
  &-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;
  &.modal-zindex-top{
    z-index: 20000;
  }
}
 
 
{# /* // Forms */ #}
 
.form-group{
  @extend %element-margin;
  .form-label{
    float: left;
    width: 100%;
    margin-bottom: 10px;
  }
  .alert{
    margin: 10px 0 0 0;
  }
}
 
.form-select {
  display: block;
  width: 100%;
  &:focus{
    outline:0;
  }
  &::-ms-expand {
    display: none;
  }
}
 
{#/*============================================================================
  #Media queries
==============================================================================*/ #}
 
{# /* // Min width 768px */ #}
 
@media (min-width: 768px) { 
 
  {# /* //// Components */ #}
 
  {# /* Modals */ #}
 
 .modal{
    &-centered{
      height: 80%;
      width: 80%;
      left: 10%;
      margin: 5% auto;
      &-small{
        left: 50%;
        width: 30%;
        height: auto;
        max-height: 80%;
        margin: 0;
      }
    }
    &-docked-md{
      width: 500px;
      &-centered{
        left: calc(50% - 250px);
        bottom: auto;
        height: auto;
      }
    }
    &-bottom-sheet {
      top: 100%;
      &.modal-show {
        top: 0;
        bottom: auto;
      }
    }
    &-docked-small{
      width: 350px;
    }
  }

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). El código que necesitamos es el siguiente:

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

{# /* // Lang select */ #}

changeLang = function(element) {
    var selected_country_url = element.find("option").filter((el) => el.selected).attr("data-country-url");
    location.href = selected_country_url;
};

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

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

Traducciones

En este paso agregamos los textos para las traducciones en el archivo config/translations.txt

es "Aplicar"
pt "Aplicar"
en "Apply"
es_mx "Aplicar"

es "Aplicando..."
pt "Aplicando..."
en "Aplying..."
es_mx "Aplicando..."

es "No encontramos este código postal{1}"
pt "Não conseguimos encontrar esse CEP{1}"
en "We couldn&#39;t find this zipcode{1}"
es_mx "No encontramos este código postal{1}"

es " para "
pt " para "
en " for "
es_mx " para "

es ". ¿Está bien escrito?"
pt ". Está bem escrito?"
en ". Is it written right?"
es_mx ". ¿Está bien escrito?"

es ". Podés intentar con otro o"
pt ". Pode tentar outro ou"
en ". You can try another or"
es_mx ". Puedes intentar con otro o"

es "cambiar tu país de entrega"
pt "alterar o país de entrega"
en "change your shipping country"
es_mx "cambiar tu país de entrega"

es "País de entrega"
pt "Pais de entrega"
en "Shipping country"
es_mx "País de entrega"

es "País donde entregaremos tu compra"
pt "País onde entregaremos seu pedido"
en "Country where we will deliver your order"
es_mx "País donde entregaremos tu 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 "No encontramos este código postal para "
pt "Não conseguimos encontrar esse CEP para"
en "We couldn&#39;t find this zipcode for"
es_mx "No encontramos este código postal para"

Activación

Por último recordá que para tener más de un idioma en tu tienda debes hacerlo desde la parte de “Configuraciones/Idiomas y monedas”.