Member pricing technical details

One of the most popular membership program benefits is offering members exclusive discounts on products. This helps increase customer lifetime value and incentivizes members to regularly come back to the storefront and make new purchases.

In this guide, we walk through one way to implement this benefit using Shopify liquid code snippets and Shopify Scripts.

🚧

Before you start

  • This is a supplemental resource for Recharge's member pricing guide. Refer to Membership benefits: Member pricing for step by step instructions for configuration.
  • This build requires advanced knowledge of Shopify liquid and some basic knowledge of Ruby. This is not part of Recharge's standard turnkey solution.
  • This build utilizes the Shopify Script Editor which is only available to Shopify Plus merchants.
  • This solution relies on the membership tags created by the Recharge membership program.
  • The examples in this guide use the Dawn Shopify Online Store 2.0 theme files. We strongly recommend using a 2.0 theme when building out your membership program.
  • We recommend applying changes in a duplicate copy of your current theme to avoid causing issues with your live store.

Update product and collection pages

First, add code to the product page liquid files to display the discounted member pricing.

  1. Go to Sales channels > Online Store > Themes and click customize on the duplicate copy of your current theme.
  2. Click the ... button in the top navigation bar and select Edit code.
357
  1. Go to Snippets > price.liquid
  2. Move line 83 </div> down to line 92 so it is relocated to the line above </div> on the last line of the file. See the examples below showing the code before and after making the change.
</small>
  </div>
  {%- if show_badges -%}
    <span class="badge price__badge-sale color-{{ settings.sale_badge_color_scheme }}">
      {{ 'products.product.on_sale' | t }}
    </span>

    <span class="badge price__badge-sold-out color-{{ settings.sold_out_badge_color_scheme }}">
      {{ 'products.product.sold_out' | t }}
    </span>
  {%- endif -%}
</div>
</small>
    {%- if show_badges -%}
      <span class="badge price__badge-sale color-{{ settings.sale_badge_color_scheme }}">
        {{ 'products.product.on_sale' | t }}
      </span>
  
      <span class="badge price__badge-sold-out color-{{ settings.sold_out_badge_color_scheme }}">
        {{ 'products.product.sold_out' | t }}
      </span>
    {%- endif -%}
  </div>
</div>
  1. Insert the member pricing code snippet (found below) between line 91 and line 92.
{%- comment -%}
  Member Program Info - v1.5.0
{%- endcomment -%}
{%- liquid
  if use_variant
    assign target = product.selected_or_first_available_variant
  else
    assign target = product
  endif
  assign price = target.price | default: 1999
  
  ##### CONFIGURATION 

  ### Default member price messaging

  ## Member price label(Example: Member price, VIP price, Rewards price, Club price)
  assign memberPriceLabel = 'Member price'

  ## Determines if the member price discount is displayed as `$ off` or `% off` (Possible values: amount, percentage)
  assign discountDisplayStyle = 'amount'

  ## Member price messaging for guests (unauthenticated shoppers)
  assign defaultMessageForGuests = 'Member price available'

  ## Member price messaging for customers (authenticated shoppers)
  assign defaultMessageForCustomers = 'Member price available'

  ## Member price messaging when products contain OTP and SUB discounts
  assign defaultMessageForMultiDiscounts = 'Member price available'

  ### Default display options

  ## Determines the default member price displayed to non-members (Use membership program customer tag)
  assign customerTag = 'rc-member-default-program-name'

  ## Determines what should be displayed to guests(Possible values: price, defaultMessageForGuests)
  assign guestDefault = 'price'

  ## Determines what should be displayed to customers(Possible values: price, defaultMessageForCustomers)
  assign customerDefault = 'defaultMessageForCustomers'

  ## Determines what should be displayed when product contains OTP and SUB discounts(Possible values: otp, sub, defaultMessageForMultiDiscounts)
  assign multiDiscountDefault = 'otp'

  ##### END CONFIGURATION

  assign isCustomer = false
  if customer
    assign isCustomer = true
    for c_tag in customer.tags
      if c_tag contains 'rc-member'
        if c_tag contains '-active'
          assign customerTag = c_tag | remove: '-active'
        elsif c_tag contains '-inactive'
          assign customerTag = c_tag | remove: '-inactive'
        else
          assign customerTag = c_tag
        endif
        break
      endif
    endfor
  endif

  for p_tag in product.tags
    if p_tag contains customerTag
      assign program = p_tag | split: '|'
      if customerTag == program.first
        assign productTag = p_tag
        break
      endif
    endif
  endfor

  ## Generates unique number for member pricing snippet
  assign min = 10
  assign max = 100
  assign diff = max | minus: min 
  assign randNum = "now" | date: "%N" | modulo: diff | plus: min 
  assign memberPriceId = randNum | times: product.id 

-%}

<style>
  /* styles for discount amounts (e.g., 20% Off)*/
  .rc-member-price span i,
  .rc_widget__price.rc_widget__price--onetime span,
  .rc_widget__price.rc_widget__price--subsave span,
  .rc-m-discount {
    display: inline-block;
    color: red;
    font-weight: normal;
    font-style: italic;
    font-size: 14px;
  }
  .rc-member-discount{
    font-size: 1.5rem;
    line-height: 1;
  }
  /* styles for default message */
  .rc-default-message{margin:0;font-style:italic;}
</style>
<div class="price__member price__member-{{ product.id }} price__member-{{ memberPriceId }}" 
  data-rc-product-tag="{{ productTag }}" 
  data-rc-customer-tag="{{ customerTag }}" 
  data-rc-member-text="{{ memberPriceLabel }}"
  data-shpfy-currency="{{ shop.currency }}" 
  data-shpfy-currency-symbol="{{ cart.currency.symbol }}" 
  data-shpfy-initial-price="{{ price | money_without_currency }}"
  data-rc-show-dollar-savings="{{ discountDisplayStyle }}"
  data-multi-discount-display="{{ multiDiscountDefault }}"
>
  <div class="price-item--member">
    <div style="display:flex;">
      <p style="font-weight:bold; margin:0;" class="rc-member-price-container">
        <span class="rc-member-price"></span>
      </p>
    </div>
  </div>
</div>

<!-- // Displays default messaging  -->
<p class="rc-default-message-{{ product.id }} rc-default-message-{{ memberPriceId }} rc-default-message"></p>

<script type="text/javascript">
  (function () {
    const uniqPriceNum = "{{ memberPriceId }}";
    const memberClass = `.price__member-${ uniqPriceNum }`;
    const memberElement = document.querySelector(memberClass);
    const priceClass = `.price__member-{{ product.id }}.price__member-${ uniqPriceNum } .rc-member-price`;
    const membershipProductTag = memberElement.getAttribute('data-rc-product-tag');
    const productTagSplit = membershipProductTag.split('|');
    const customerTag = memberElement.getAttribute('data-rc-customer-tag');
    const memberPriceLabel = memberElement.getAttribute('data-rc-member-text');
    const rcMessage = document.querySelector(`.rc-default-message-${ uniqPriceNum }`);
    const initialPrice = Number(memberElement.getAttribute('data-shpfy-initial-price').replace(/[^0-9\.-]+/g,""));
    const discountDisplayStyle = memberElement.getAttribute('data-rc-show-dollar-savings');
    // default message
    const defaultMessageForGuests = '{{ defaultMessageForGuests }}';
    const defaultMessageForCustomers = '{{ defaultMessageForCustomers }}';
    const multiDiscountDefault = memberElement.getAttribute('data-multi-discount-display');
    const defaultMessageForMultiDiscounts = '{{ defaultMessageForMultiDiscounts }}';
    const guestDefault = '{{ guestDefault }}';
    const customerDefault = '{{ customerDefault }}';
    const isCustomer = '{{ isCustomer }}';
    const widgetStyle = document.createElement('style');
    // injects widget styles
    widgetStyle.innerHTML = `
      .rc-radio.rc_widget__option--subsave .rc_widget__option__label.rc-radio__label,
      .rc-radio.rc_widget__option--onetime .rc_widget__option__label.rc-radio__label {
        display: inline;
        font-weight:normal;
        font-size:1.5rem;
        margin-bottom: 3px;
        margin-left: 5px;
      }
      .rc_widget__option__selector{
        display:flex;
      }
      .rc-radio .rc-radio__label {
        margin:0;
        flex-wrap: wrap;
      }
      .rc-radio .rc-radio__label b,
      .rc-radio .rc-radio__label span,
      .rc-checkbox__descriptor,
      .rc-option__descriptor{
        display: inline;
      }
      .rc-template__button-group .rc-radio .rc-radio__label b,
      .rc-template__button-group .rc-radio .rc-radio__label span,
      .rc-checkbox__label {
        line-height: 1.5;
      }
      .rc-template__radio-group .rc-radio .rc-radio__label{
        display: block;
      }
    `;
    
    //checks if purchase types exist
    function indexOfPurchaseType(string) {
      const subTypeIndex = productTagSplit.findIndex(element => {
        if (element.includes(string)) { return true; }
      });
      return subTypeIndex;
    }
    //splits the discount string
    function discountTagSplit( indexOfType, subType){
      const discountInfo = productTagSplit[indexOfType].split(':');
      const discountAmount = discountInfo[1];
      const discountType = discountInfo[2];
      memberElement.setAttribute( `rc-${subType}-discount-amount`, discountAmount );
      memberElement.setAttribute( `rc-${subType}-discount-type`, discountType );
    }
    //updates the member info in header on PDP or tile footer in PCP
    function updateMemberInfo(){
      const otp = indexOfPurchaseType('otp');
      const sub = indexOfPurchaseType('sub');
      // set default message
      if(membershipProductTag){
        if (productTagSplit.length > 3) {
          discountTagSplit(otp, 'otp'); 
          discountTagSplit(sub, 'sub'); 
          switch(multiDiscountDefault){
            case 'otp':
              updateMemberPrice('otp', priceClass);
              break;
            case 'sub':
              updateMemberPrice('sub', priceClass);
              break;
            case 'defaultMessageForMultiDiscounts':
              memberElement.style.display = "none"; 
              rcMessage.innerHTML = defaultMessageForMultiDiscounts;
              break;
          }
        } else if(productTagSplit.length < 4) {
          if (otp != -1) { 
            discountTagSplit(otp, 'otp'); 
            updateMemberPrice('otp', priceClass);
          }
          if (sub != -1) { 
            discountTagSplit(sub, 'sub'); 
            updateMemberPrice('sub', priceClass);
          }
        }
        if (isCustomer === 'true'){
          if ( customerDefault === 'defaultMessageForCustomers'){
            document.querySelector(priceClass).style.display  = 'none';
            rcMessage.innerHTML = defaultMessageForCustomers
          }
        } else if (isCustomer === 'false') {
          if ( guestDefault === 'defaultMessageForGuests'){
            document.querySelector(priceClass).style.display  = 'none';
            rcMessage.innerHTML = defaultMessageForGuests
          } 
        }
      }
    }

    // format for money
    function formatMoney (price, currencyAndMoney = false){
      const currencyCode = memberElement.getAttribute('data-shpfy-currency');
      const currencySymbol = memberElement.getAttribute('data-shpfy-currency-symbol');
      return `${currencySymbol}${ Number(price).toFixed(2)} ${(currencyAndMoney) ? ` ${currencyCode}`: ''}`;
    }

    // calculates and displays member price on PDP and PCP
    function updateMemberPrice(purchaseType, container, subDiscount, showCurrencyCode = true, widgetUpdate = false) {
      if (membershipProductTag){
        const membershipPrice = document.querySelector(container);
        const discountAmount = Number(memberElement.getAttribute(`rc-${purchaseType}-discount-amount`));
        const discountType = memberElement.getAttribute(`rc-${purchaseType}-discount-type`);
        let totalAmount = discountAmount;
        let memberLanguage =  memberPriceLabel ? memberPriceLabel : 'Member price';
        let discountLanguage = '';
        let displayPrice;

        if (discountType === 'percent') {
          const percentage = totalAmount * 0.01;
          const percentDiscountPrice = initialPrice * percentage;
          const newDiscountPrice = initialPrice - percentDiscountPrice;
          finalPrice =  Number(newDiscountPrice).toFixed(2);
          // initial discount from recharge subs
          if (subDiscount) {
            const subAmountOff = ( subDiscount * .01 ) * newDiscountPrice;
            finalPrice =  Number(newDiscountPrice - subAmountOff).toFixed(2);
          }
          // adds in the new html with discount
          discountLanguage = (discountDisplayStyle === 'amount')
            ? `$${ Number(initialPrice - finalPrice).toFixed(2)} Off`
            : `${totalAmount}% Off`;
        } else if (discountType == 'fixed') {
          totalAmount = initialPrice - discountAmount;
          finalPrice =  Number(totalAmount).toFixed(2);
          // adds in the new html with disount
          discountLanguage = `$${discountAmount} Off`

          // initial discount from recharge subs
          if (subDiscount) {
            totalAmount = (initialPrice - discountAmount) - (initialPrice * (subDiscount * 0.01));
            finalPrice =  Number(totalAmount).toFixed(2);
            // adds in the new html with discount
            discountLanguage = (discountDisplayStyle === 'amount')
            ? `$${ Number(initialPrice - finalPrice).toFixed(2)} Off`
            : `${subDiscount}% + $${discountAmount} Off`;
          }
        }

        // shows or hides currency code
        displayPrice = showCurrencyCode ? formatMoney(finalPrice, true) : formatMoney(finalPrice);
        // adds in the new html with disount
        let newMemberDiv = `<div class="rc-member-discount">
            <span>
              <b>${memberLanguage}: ${ displayPrice }</b>&nbsp;<i class="rc-m-discount">${discountLanguage}</i>
            </span>
          </div>`;
        if (widgetUpdate === true){
          membershipPrice.innerHTML += newMemberDiv;
        } 
        else {
          // checks if price already exists
          document.querySelector(`${ priceClass } .rc-member-discount`)
          ? membershipPrice.innerHTML = newMemberDiv
          : membershipPrice.innerHTML += newMemberDiv;
        }
      }
    }

    // Changes prices on page load
    updateMemberInfo();

    {% unless request.page_type == 'collection' %}
      // Updates prices in widget on widget load
      const startTime = new Date().getTime();
      const checkExist = setInterval(function() {
        // Run check for 90sec, then clear
        if(new Date().getTime() - startTime > 90000){
          clearInterval(checkExist);
          return;
        }
        // Check if RC widget exists on the page
        if (document.querySelector('.rc-widget')) {
          // Clear interval so this only runs once
          clearInterval(checkExist);
          document.head.appendChild(widgetStyle);
          const displayPrice = document.querySelector('.price-item--regular.recharge-inner-most-price');
          const regexDiscount = /\d+/;
          // 2.0 widget templates
          const radio = document.querySelector('.rc-template__radio');
          const radioGroup = document.querySelector('.rc-template__radio-group');
          const buttonGroup = document.querySelector('.rc-template__button-group');
          const checkbox = document.querySelector('.rc-template__checkbox .rc-checkbox');
          // legacy widget
          const radioLegacy = document.querySelector('.rc-template__legacy-radio');
          // sub only
          const subOnly2_0 = document.querySelector('.rc-subscription-only');

          // shows initial price in header outside widget
          if (displayPrice){ displayPrice.style.display = 'none'; }
          document.querySelector('.price__regular').innerHTML = `<span class="rc-initial-price">${formatMoney(initialPrice, true)}</span>`;

          function getSubDiscount(discElement){
            const discountString = document.querySelector(discElement).innerHTML;
            let rcSubDiscountAmount = 0;
            if (discountString.match(regexDiscount)){
              rcSubDiscountAmount = discountString.match(regexDiscount)[0];
            }
            return rcSubDiscountAmount;
          }
          // checks widget template and displays discounts
          function updatedWidgetPrices (discElement, subElement, otpElement) {
            const subDiscountAmount = getSubDiscount(discElement);
            const subAmount = memberElement.getAttribute('rc-sub-discount-amount');
            const subType = memberElement.getAttribute('rc-sub-discount-type');
            const otpAmount = memberElement.getAttribute('rc-otp-discount-amount');
            const otpType = memberElement.getAttribute('rc-otp-discount-type');
            
            if (subElement && subAmount){ 
              updateMemberPrice('sub', subElement, subDiscountAmount, false, true);
              document.querySelector(priceClass).innerHTML = '';
              updateMemberPrice('sub', priceClass, subDiscountAmount, false);
            }
            if (otpElement && otpAmount){ 
              updateMemberPrice('otp', otpElement, 0, false, true);
              document.querySelector(priceClass).innerHTML = '';
              updateMemberPrice('otp', priceClass, 0, false);
            }
          }
          function subOnlyView(){
            document.querySelector(priceClass).innerHTML = '';
            updateMemberPrice('sub', priceClass);
          }
          function conditionalRender(template, attr, legacyArr, twoOArr){
            let subDiscountAmount = 0;
            document.querySelector(priceClass).innerHTML = '';
            if (template.hasAttribute(attr)){
              if (template.querySelector('input')) {
                // legacy template
                updatedWidgetPrices(legacyArr[0],legacyArr[1], legacyArr[2]);
              } else {
                // Sub only
                subOnlyView()
              }
            } else {
              // 2.0
              updatedWidgetPrices(twoOArr[0], twoOArr[1], twoOArr[2]);
            }
          }

          // check widget template type 
          let legacyParams = [], twoOParams = [];
          const twoOOTPLabel = '.onetime-radio .rc-radio__label';
          const twoOSubLabel = '.subscription-radio .rc-radio__label';
          const legacyOTPLabel = '.rc-option__onetime .rc_widget__option__label';
          const legacySubLabel = '.rc-option__subsave .rc_widget__option__label';
          switch(true){
            case !!subOnly2_0:
              subOnlyView()
              break;
            case !!radioLegacy:
              legacyParams = ['.rc-template__legacy-radio .rc-option__discount', legacySubLabel, legacyOTPLabel];
              conditionalRender( radioLegacy, "data-template-legacy-radio", legacyParams );
              break;
            case !!radio:
              updatedWidgetPrices('.rc-radio__subscription', twoOSubLabel, twoOOTPLabel);
              break;
            case !!radioGroup:
              legacyParams = ['.rc-template__radio-group .rc_widget__option__discount','.rc-option__subsave .rc-radio__label', '.rc-option__onetime .rc-radio__label'];
              twoOParams = ['.rc-template__radio-group .subscription-radio .discount-label', twoOSubLabel, twoOOTPLabel];
              conditionalRender( radioGroup, "data-template-radio-group", legacyParams, twoOParams );
              break;
            case !!buttonGroup:
              legacyParams = ['.rc-template__button-group .rc_widget__option__discount', legacySubLabel, legacyOTPLabel];
              twoOParams = ['.rc-template__button-group .subscription-radio .discount-label', twoOSubLabel, twoOOTPLabel];
              conditionalRender( buttonGroup, "data-template-button-group", legacyParams, twoOParams );
              break;
            case !!checkbox:
              legacyParams = ['.rc-template__checkbox .rc_widget__option__discount', '.rc-checkbox__label'];
              twoOParams = ['.rc-checkbox__subscription', '.rc-checkbox__subscription'];
              conditionalRender( checkbox, "data-option-subsave", legacyParams, twoOParams );
              break;
            default:
              break;
          }
        } 
      }, 100);
    {% endunless %}
  })();
</script>
{%- comment -%}
  End of Member Program Info
{%- endcomment -%}

👍

Example file

If using Dawn Shopify Theme 2.0 with no customizations, the final price.liquid file should look like the example below:

{% comment %}
    Renders a list of product's price (regular, sale)

    Accepts:
    - product: {Object} Product Liquid object (optional)
    - use_variant: {Boolean} Renders selected or first variant price instead of overall product pricing (optional)
    - show_badges: {Boolean} Renders 'Sale' and 'Sold Out' tags if the product matches the condition (optional)
    - price_class: {String} Adds a price class to the price element (optional)

    Usage:
    {% render 'price', product: product %}
{% endcomment %}
{%- liquid
  if use_variant
    assign target = product.selected_or_first_available_variant
  else
    assign target = product
  endif

  assign compare_at_price = target.compare_at_price
  assign price = target.price | default: 1999
  assign available = target.available | default: false
  assign money_price = price | money
  if settings.currency_code_enabled
    assign money_price = price | money_with_currency
  endif

  if target == product and product.price_varies
    assign money_price = 'products.product.price.from_price_html' | t: price: money_price
  endif
-%}

<div class="price
  {%- if price_class %} {{ price_class }}{% endif -%}
  {%- if available == false %} price--sold-out {% endif -%}
  {%- if compare_at_price > price %} price--on-sale {% endif -%}
  {%- if product.price_varies == false and product.compare_at_price_varies %} price--no-compare{% endif -%}
  {%- if show_badges %} price--show-badge{% endif -%}">
  <div class="price__container">
    {%- comment -%}
      Explanation of description list:
        - div.price__regular: Displayed when there are no variants on sale
        - div.price__sale: Displayed when a variant is a sale
    {%- endcomment -%}
    <div class="price__regular">
      <span class="visually-hidden visually-hidden--inline">{{ 'products.product.price.regular_price' | t }}</span>
      <span class="price-item price-item--regular">
        {{ money_price }}
      </span>
    </div>
    <div class="price__sale">
      {%- unless product.price_varies == false and product.compare_at_price_varies %}
        <span class="visually-hidden visually-hidden--inline">{{ 'products.product.price.regular_price' | t }}</span>
        <span>
          <s class="price-item price-item--regular">
            {% if settings.currency_code_enabled %}
              {{ compare_at_price | money_with_currency }}
            {% else %}
              {{ compare_at_price | money }}
            {% endif %}
          </s>
        </span>
      {%- endunless -%}
      <span class="visually-hidden visually-hidden--inline">{{ 'products.product.price.sale_price' | t }}</span>
      <span class="price-item price-item--sale price-item--last">
        {{ money_price }}
      </span>
    </div>
    <small class="unit-price caption{% if product.selected_or_first_available_variant.unit_price_measurement == nil %} hidden{% endif %}">
      <span class="visually-hidden">{{ 'products.product.price.unit_price' | t }}</span>
      <span class="price-item price-item--last">
        <span>{{- product.selected_or_first_available_variant.unit_price | money -}}</span>
        <span aria-hidden="true">/</span>
        <span class="visually-hidden">&nbsp;{{ 'accessibility.unit_price_separator' | t }}&nbsp;</span>
        <span>
          {%- if product.selected_or_first_available_variant.unit_price_measurement.reference_value != 1 -%}
            {{- product.selected_or_first_available_variant.unit_price_measurement.reference_value -}}
          {%- endif -%}
          {{ product.selected_or_first_available_variant.unit_price_measurement.reference_unit }}
        </span>
      </span>
    </small>
  {%- if show_badges -%}
    <span class="badge price__badge-sale color-{{ settings.sale_badge_color_scheme }}">
      {{ 'products.product.on_sale' | t }}
    </span>

    <span class="badge price__badge-sold-out color-{{ settings.sold_out_badge_color_scheme }}">
      {{ 'products.product.sold_out' | t }}
    </span>
  {%- endif -%}
{%- comment -%}
  Member Program Info - v1.5.0
{%- endcomment -%}
{%- liquid
  if use_variant
    assign target = product.selected_or_first_available_variant
  else
    assign target = product
  endif
  assign price = target.price | default: 1999
  
  ##### CONFIGURATION 

  ### Default member price messaging

  ## Member price label(Example: Member price, VIP price, Rewards price, Club price)
  assign memberPriceLabel = 'Member price'

  ## Determines if the member price discount is displayed as `$ off` or `% off` (Possible values: amount, percentage)
  assign discountDisplayStyle = 'amount'

  ## Member price messaging for guests (unauthenticated shoppers)
  assign defaultMessageForGuests = 'Member price available'

  ## Member price messaging for customers (authenticated shoppers)
  assign defaultMessageForCustomers = 'Member price available'

  ## Member price messaging when products contain OTP and SUB discounts
  assign defaultMessageForMultiDiscounts = 'Member price available'

  ### Default display options

  ## Determines the default member price displayed to non-members (Use membership program customer tag)
  assign customerTag = 'rc-member-default-program-name'

  ## Determines what should be displayed to guests(Possible values: price, defaultMessageForGuests)
  assign guestDefault = 'price'

  ## Determines what should be displayed to customers(Possible values: price, defaultMessageForCustomers)
  assign customerDefault = 'defaultMessageForCustomers'

  ## Determines what should be displayed when product contains OTP and SUB discounts(Possible values: otp, sub, defaultMessageForMultiDiscounts)
  assign multiDiscountDefault = 'otp'

  ##### END CONFIGURATION

  assign isCustomer = false
  if customer
    assign isCustomer = true
    for c_tag in customer.tags
      if c_tag contains 'rc-member'
        if c_tag contains '-active'
          assign customerTag = c_tag | remove: '-active'
        elsif c_tag contains '-inactive'
          assign customerTag = c_tag | remove: '-inactive'
        else
          assign customerTag = c_tag
        endif
        break
      endif
    endfor
  endif

  for p_tag in product.tags
    if p_tag contains customerTag
      assign program = p_tag | split: '|'
      if customerTag == program.first
        assign productTag = p_tag
        break
      endif
    endif
  endfor

  ## Generates unique number for member pricing snippet
  assign min = 10
  assign max = 100
  assign diff = max | minus: min 
  assign randNum = "now" | date: "%N" | modulo: diff | plus: min 
  assign memberPriceId = randNum | times: product.id 

-%}

<style>
  /* styles for discount amounts (e.g., 20% Off)*/
  .rc-member-price span i,
  .rc_widget__price.rc_widget__price--onetime span,
  .rc_widget__price.rc_widget__price--subsave span,
  .rc-m-discount {
    display: inline-block;
    color: red;
    font-weight: normal;
    font-style: italic;
    font-size: 14px;
  }
  .rc-member-discount{
    font-size: 1.5rem;
    line-height: 1;
  }
  /* styles for default message */
  .rc-default-message{margin:0;font-style:italic;}
</style>
<div class="price__member price__member-{{ product.id }} price__member-{{ memberPriceId }}" 
  data-rc-product-tag="{{ productTag }}" 
  data-rc-customer-tag="{{ customerTag }}" 
  data-rc-member-text="{{ memberPriceLabel }}"
  data-shpfy-currency="{{ shop.currency }}" 
  data-shpfy-currency-symbol="{{ cart.currency.symbol }}" 
  data-shpfy-initial-price="{{ price | money_without_currency }}"
  data-rc-show-dollar-savings="{{ discountDisplayStyle }}"
  data-multi-discount-display="{{ multiDiscountDefault }}"
>
  <div class="price-item--member">
    <div style="display:flex;">
      <p style="font-weight:bold; margin:0;" class="rc-member-price-container">
        <span class="rc-member-price"></span>
      </p>
    </div>
  </div>
</div>

<!-- // Displays default messaging  -->
<p class="rc-default-message-{{ product.id }} rc-default-message-{{ memberPriceId }} rc-default-message"></p>

<script type="text/javascript">
  (function () {
    const uniqPriceNum = "{{ memberPriceId }}";
    const memberClass = `.price__member-${ uniqPriceNum }`;
    const memberElement = document.querySelector(memberClass);
    const priceClass = `.price__member-{{ product.id }}.price__member-${ uniqPriceNum } .rc-member-price`;
    const membershipProductTag = memberElement.getAttribute('data-rc-product-tag');
    const productTagSplit = membershipProductTag.split('|');
    const customerTag = memberElement.getAttribute('data-rc-customer-tag');
    const memberPriceLabel = memberElement.getAttribute('data-rc-member-text');
    const rcMessage = document.querySelector(`.rc-default-message-${ uniqPriceNum }`);
    const initialPrice = Number(memberElement.getAttribute('data-shpfy-initial-price').replace(/[^0-9\.-]+/g,""));
    const discountDisplayStyle = memberElement.getAttribute('data-rc-show-dollar-savings');
    // default message
    const defaultMessageForGuests = '{{ defaultMessageForGuests }}';
    const defaultMessageForCustomers = '{{ defaultMessageForCustomers }}';
    const multiDiscountDefault = memberElement.getAttribute('data-multi-discount-display');
    const defaultMessageForMultiDiscounts = '{{ defaultMessageForMultiDiscounts }}';
    const guestDefault = '{{ guestDefault }}';
    const customerDefault = '{{ customerDefault }}';
    const isCustomer = '{{ isCustomer }}';
    const widgetStyle = document.createElement('style');
    // injects widget styles
    widgetStyle.innerHTML = `
      .rc-radio.rc_widget__option--subsave .rc_widget__option__label.rc-radio__label,
      .rc-radio.rc_widget__option--onetime .rc_widget__option__label.rc-radio__label {
        display: inline;
        font-weight:normal;
        font-size:1.5rem;
        margin-bottom: 3px;
        margin-left: 5px;
      }
      .rc_widget__option__selector{
        display:flex;
      }
      .rc-radio .rc-radio__label {
        margin:0;
        flex-wrap: wrap;
      }
      .rc-radio .rc-radio__label b,
      .rc-radio .rc-radio__label span,
      .rc-checkbox__descriptor,
      .rc-option__descriptor{
        display: inline;
      }
      .rc-template__button-group .rc-radio .rc-radio__label b,
      .rc-template__button-group .rc-radio .rc-radio__label span,
      .rc-checkbox__label {
        line-height: 1.5;
      }
      .rc-template__radio-group .rc-radio .rc-radio__label{
        display: block;
      }
    `;
    
    //checks if purchase types exist
    function indexOfPurchaseType(string) {
      const subTypeIndex = productTagSplit.findIndex(element => {
        if (element.includes(string)) { return true; }
      });
      return subTypeIndex;
    }
    //splits the discount string
    function discountTagSplit( indexOfType, subType){
      const discountInfo = productTagSplit[indexOfType].split(':');
      const discountAmount = discountInfo[1];
      const discountType = discountInfo[2];
      memberElement.setAttribute( `rc-${subType}-discount-amount`, discountAmount );
      memberElement.setAttribute( `rc-${subType}-discount-type`, discountType );
    }
    //updates the member info in header on PDP or tile footer in PCP
    function updateMemberInfo(){
      const otp = indexOfPurchaseType('otp');
      const sub = indexOfPurchaseType('sub');
      // set default message
      if(membershipProductTag){
        if (productTagSplit.length > 3) {
          discountTagSplit(otp, 'otp'); 
          discountTagSplit(sub, 'sub'); 
          switch(multiDiscountDefault){
            case 'otp':
              updateMemberPrice('otp', priceClass);
              break;
            case 'sub':
              updateMemberPrice('sub', priceClass);
              break;
            case 'defaultMessageForMultiDiscounts':
              memberElement.style.display = "none"; 
              rcMessage.innerHTML = defaultMessageForMultiDiscounts;
              break;
          }
        } else if(productTagSplit.length < 4) {
          if (otp != -1) { 
            discountTagSplit(otp, 'otp'); 
            updateMemberPrice('otp', priceClass);
          }
          if (sub != -1) { 
            discountTagSplit(sub, 'sub'); 
            updateMemberPrice('sub', priceClass);
          }
        }
        if (isCustomer === 'true'){
          if ( customerDefault === 'defaultMessageForCustomers'){
            document.querySelector(priceClass).style.display  = 'none';
            rcMessage.innerHTML = defaultMessageForCustomers
          }
        } else if (isCustomer === 'false') {
          if ( guestDefault === 'defaultMessageForGuests'){
            document.querySelector(priceClass).style.display  = 'none';
            rcMessage.innerHTML = defaultMessageForGuests
          } 
        }
      }
    }

    // format for money
    function formatMoney (price, currencyAndMoney = false){
      const currencyCode = memberElement.getAttribute('data-shpfy-currency');
      const currencySymbol = memberElement.getAttribute('data-shpfy-currency-symbol');
      return `${currencySymbol}${ Number(price).toFixed(2)} ${(currencyAndMoney) ? ` ${currencyCode}`: ''}`;
    }

    // calculates and displays member price on PDP and PCP
    function updateMemberPrice(purchaseType, container, subDiscount, showCurrencyCode = true, widgetUpdate = false) {
      if (membershipProductTag){
        const membershipPrice = document.querySelector(container);
        const discountAmount = Number(memberElement.getAttribute(`rc-${purchaseType}-discount-amount`));
        const discountType = memberElement.getAttribute(`rc-${purchaseType}-discount-type`);
        let totalAmount = discountAmount;
        let memberLanguage =  memberPriceLabel ? memberPriceLabel : 'Member price';
        let discountLanguage = '';
        let displayPrice;

        if (discountType === 'percent') {
          const percentage = totalAmount * 0.01;
          const percentDiscountPrice = initialPrice * percentage;
          const newDiscountPrice = initialPrice - percentDiscountPrice;
          finalPrice =  Number(newDiscountPrice).toFixed(2);
          // initial discount from recharge subs
          if (subDiscount) {
            const subAmountOff = ( subDiscount * .01 ) * newDiscountPrice;
            finalPrice =  Number(newDiscountPrice - subAmountOff).toFixed(2);
          }
          // adds in the new html with discount
          discountLanguage = (discountDisplayStyle === 'amount')
            ? `$${ Number(initialPrice - finalPrice).toFixed(2)} Off`
            : `${totalAmount}% Off`;
        } else if (discountType == 'fixed') {
          totalAmount = initialPrice - discountAmount;
          finalPrice =  Number(totalAmount).toFixed(2);
          // adds in the new html with disount
          discountLanguage = `$${discountAmount} Off`

          // initial discount from recharge subs
          if (subDiscount) {
            totalAmount = (initialPrice - discountAmount) - (initialPrice * (subDiscount * 0.01));
            finalPrice =  Number(totalAmount).toFixed(2);
            // adds in the new html with discount
            discountLanguage = (discountDisplayStyle === 'amount')
            ? `$${ Number(initialPrice - finalPrice).toFixed(2)} Off`
            : `${subDiscount}% + $${discountAmount} Off`;
          }
        }

        // shows or hides currency code
        displayPrice = showCurrencyCode ? formatMoney(finalPrice, true) : formatMoney(finalPrice);
        // adds in the new html with disount
        let newMemberDiv = `<div class="rc-member-discount">
            <span>
              <b>${memberLanguage}: ${ displayPrice }</b>&nbsp;<i class="rc-m-discount">${discountLanguage}</i>
            </span>
          </div>`;
        if (widgetUpdate === true){
          membershipPrice.innerHTML += newMemberDiv;
        } 
        else {
          // checks if price already exists
          document.querySelector(`${ priceClass } .rc-member-discount`)
          ? membershipPrice.innerHTML = newMemberDiv
          : membershipPrice.innerHTML += newMemberDiv;
        }
      }
    }

    // Changes prices on page load
    updateMemberInfo();

    {% unless request.page_type == 'collection' %}
      // Updates prices in widget on widget load
      const startTime = new Date().getTime();
      const checkExist = setInterval(function() {
        // Run check for 90sec, then clear
        if(new Date().getTime() - startTime > 90000){
          clearInterval(checkExist);
          return;
        }
        // Check if RC widget exists on the page
        if (document.querySelector('.rc-widget')) {
          // Clear interval so this only runs once
          clearInterval(checkExist);
          document.head.appendChild(widgetStyle);
          const displayPrice = document.querySelector('.price-item--regular.recharge-inner-most-price');
          const regexDiscount = /\d+/;
          // 2.0 widget templates
          const radio = document.querySelector('.rc-template__radio');
          const radioGroup = document.querySelector('.rc-template__radio-group');
          const buttonGroup = document.querySelector('.rc-template__button-group');
          const checkbox = document.querySelector('.rc-template__checkbox .rc-checkbox');
          // legacy widget
          const radioLegacy = document.querySelector('.rc-template__legacy-radio');
          // sub only
          const subOnly2_0 = document.querySelector('.rc-subscription-only');

          // shows initial price in header outside widget
          if (displayPrice){ displayPrice.style.display = 'none'; }
          document.querySelector('.price__regular').innerHTML = `<span class="rc-initial-price">${formatMoney(initialPrice, true)}</span>`;

          function getSubDiscount(discElement){
            const discountString = document.querySelector(discElement).innerHTML;
            let rcSubDiscountAmount = 0;
            if (discountString.match(regexDiscount)){
              rcSubDiscountAmount = discountString.match(regexDiscount)[0];
            }
            return rcSubDiscountAmount;
          }
          // checks widget template and displays discounts
          function updatedWidgetPrices (discElement, subElement, otpElement) {
            const subDiscountAmount = getSubDiscount(discElement);
            const subAmount = memberElement.getAttribute('rc-sub-discount-amount');
            const subType = memberElement.getAttribute('rc-sub-discount-type');
            const otpAmount = memberElement.getAttribute('rc-otp-discount-amount');
            const otpType = memberElement.getAttribute('rc-otp-discount-type');
            
            if (subElement && subAmount){ 
              updateMemberPrice('sub', subElement, subDiscountAmount, false, true);
              document.querySelector(priceClass).innerHTML = '';
              updateMemberPrice('sub', priceClass, subDiscountAmount, false);
            }
            if (otpElement && otpAmount){ 
              updateMemberPrice('otp', otpElement, 0, false, true);
              document.querySelector(priceClass).innerHTML = '';
              updateMemberPrice('otp', priceClass, 0, false);
            }
          }
          function subOnlyView(){
            document.querySelector(priceClass).innerHTML = '';
            updateMemberPrice('sub', priceClass);
          }
          function conditionalRender(template, attr, legacyArr, twoOArr){
            let subDiscountAmount = 0;
            document.querySelector(priceClass).innerHTML = '';
            if (template.hasAttribute(attr)){
              if (template.querySelector('input')) {
                // legacy template
                updatedWidgetPrices(legacyArr[0],legacyArr[1], legacyArr[2]);
              } else {
                // Sub only
                subOnlyView()
              }
            } else {
              // 2.0
              updatedWidgetPrices(twoOArr[0], twoOArr[1], twoOArr[2]);
            }
          }

          // check widget template type 
          let legacyParams = [], twoOParams = [];
          const twoOOTPLabel = '.onetime-radio .rc-radio__label';
          const twoOSubLabel = '.subscription-radio .rc-radio__label';
          const legacyOTPLabel = '.rc-option__onetime .rc_widget__option__label';
          const legacySubLabel = '.rc-option__subsave .rc_widget__option__label';
          switch(true){
            case !!subOnly2_0:
              subOnlyView()
              break;
            case !!radioLegacy:
              legacyParams = ['.rc-template__legacy-radio .rc-option__discount', legacySubLabel, legacyOTPLabel];
              conditionalRender( radioLegacy, "data-template-legacy-radio", legacyParams );
              break;
            case !!radio:
              updatedWidgetPrices('.rc-radio__subscription', twoOSubLabel, twoOOTPLabel);
              break;
            case !!radioGroup:
              legacyParams = ['.rc-template__radio-group .rc_widget__option__discount','.rc-option__subsave .rc-radio__label', '.rc-option__onetime .rc-radio__label'];
              twoOParams = ['.rc-template__radio-group .subscription-radio .discount-label', twoOSubLabel, twoOOTPLabel];
              conditionalRender( radioGroup, "data-template-radio-group", legacyParams, twoOParams );
              break;
            case !!buttonGroup:
              legacyParams = ['.rc-template__button-group .rc_widget__option__discount', legacySubLabel, legacyOTPLabel];
              twoOParams = ['.rc-template__button-group .subscription-radio .discount-label', twoOSubLabel, twoOOTPLabel];
              conditionalRender( buttonGroup, "data-template-button-group", legacyParams, twoOParams );
              break;
            case !!checkbox:
              legacyParams = ['.rc-template__checkbox .rc_widget__option__discount', '.rc-checkbox__label'];
              twoOParams = ['.rc-checkbox__subscription', '.rc-checkbox__subscription'];
              conditionalRender( checkbox, "data-option-subsave", legacyParams, twoOParams );
              break;
            default:
              break;
          }
        } 
      }, 100);
    {% endunless %}
  })();
</script>
{%- comment -%}
  End of Member Program Info
{%- endcomment -%}
  </div>
</div>
  1. Make updates as needed to the ##### CONFIGURATION section at the top of the snippet and click Save.

❗️

Required

The configuration below defines the customer tag for the default program that will be used to display member pricing to customers who are not active members. The customer tag for a membership program can be located on the membership program details page in the Shopify Details section.

When setting the customer tag, it should only include the characters preceding -active/-inactive

## Determines the default member price displayed to non-members (Use membership program customer tag)
assign customerTag = 'rc-member-default-program-name'

📘

Best practice

We recommend displaying this reduced price to all customers because it’s a great way to market the membership program and increase enrollment. However, if you’d rather like to limit which customers see member pricing, you can update the configuration section accordingly.

Create a Shopify Script

Next, create a Shopify Script to update the pricing in the cart and pass the discounts to checkout. The cart acts independently of the pricing pages, so this script is required to reduce prices for members in the cart to match the product pages. This script will dynamically update the cart pricing as the user takes different actions (i.e. if the user adds all items to the cart while logged out, the script will update the cart item’s prices once the member logs in).

  1. Install the Shopify Script Editor if you haven’t already. Refer to Shopify's documentation for more information on how to use the app.
  2. In the Script Editor app, click Create Script > Line Items > Blank template and click Create script.
1222
  1. Set a descriptive title and click the Code tab

  1. Delete Output.cart = Input.cart and paste the code below into the Ruby source code section.
# ================================ Script Overview ================================
# ================================================================
# v1.4.4
#
# Line Item Discounts For Members
#
# Given a Customer with a Membership tag, apply any product
#  tag-defined line item discounts to any matching product
#  in the cart.  Member Discounts can be applied to subscription
#  and/or onetime purchases as denoted by the cart
# 
# Example tags from the Recharge Membership benefit configuration:
# 
# - Customer tag:
#   rc-member-total-vip-active
#
# - Product tag:
# - rc-member-total-vip|price:member_discount|sub:10:percent|otp:15:percent
#

# ================================ Customizable Settings ================================
# ================================================================
# 
#   - MEMBER_DISCOUNT_MESSAGE is the message to be displayed to 
#     your customers during the cart and checkout process to indicate
#     why there was a discount applied.
#
# =====

MEMBER_DISCOUNT_MESSAGE = "Member Discount"

MEMBERSHIP_ENROLLMENT_PRODUCTS = [
  {
    membership_product_id: 6825939140123,
    membership_tag: "rc-member-default-program-name1"
  },
  {
    membership_product_id: 6606423163456,
    membership_tag: "rc-member-default-program-name2"
  }
]


# ================================ Script Code (do not edit) ================================
# ================================================================

SECTION_DELIMITER = "|"
VALUE_DELIMITER = ":"

class CustomerTagSelector
  def initialize()
  end
  
  def tag_match(customer_tags, product_tags)
    (customer_tags & product_tags)
  end
end

class MembershipProductSelector
  def initialize(membership_product_id)
    @product_id = membership_product_id
  end

  def match?(line_items)
    product_ids = line_items.map { |line_item| line_item.variant.product.id }
    product_ids.include? @product_id
  end
end

class MembershipDiscountor
  def initialize(discount_type, discount_amount)
    @discount_type = discount_type
    @discount_message = MEMBER_DISCOUNT_MESSAGE

    @discount_amount = if discount_type == "percent"
      1 - (discount_amount * 0.01)
    else
      Money.new(cents: 100) * discount_amount
    end

  end

  def apply(line_item)
    new_line_price = if @discount_type == "percent"
      line_item.line_price * @discount_amount
    else
      [line_item.line_price - (@discount_amount * line_item.quantity), Money.zero].max
    end

    line_item.change_line_price(new_line_price, message: @discount_message)
  end
end

class DiscountForMember
  def initialize()
  
  end

  def run(cart)
    customer = cart.customer
    anon_customer = customer.nil?
    anon_tag = ""
    customer_tags = cart.customer&.tags&.map { |t| t }
  
    process_product_tags = false
    
    # customer or enrollment product check
    process_product_tags = (!customer_tags.nil? && !customer_tags.empty?) && (customer_tags.any? { |s| s.include?('rc-member-')} && customer_tags.any? { |s| s.end_with?("-active") } )
    
    if !process_product_tags
      MEMBERSHIP_ENROLLMENT_PRODUCTS.each do |e|
        membership_product_selector = MembershipProductSelector.new(e[:membership_product_id])

      next unless (membership_product_selector.match?(cart.line_items))
        process_product_tags = true
        
        if not anon_customer
          customer_tags << e[:membership_tag]+'-active'
        else
          anon_tag = e[:membership_tag]+'-active'
        end  
      end
    end
    
    return unless process_product_tags

    cart.line_items.each do |line_item|
      product_pricing = []

      if !line_item.selling_plan_id.nil?
        purchase_type = "sub"
      else
        purchase_type = "otp"
      end
      
      line_item.variant.product.tags.each do | tag|   
        split_tags = tag.split(SECTION_DELIMITER)
        full_benefit = ""
        benefit_type = ""
        
        program_name = split_tags[0]
        if program_name.include?("rc-member-")
          
          full_benefit = split_tags[1] # assigned for use in the final output only
          benefit = full_benefit.split(VALUE_DELIMITER)
          benefit_type = benefit[1]
          
          if benefit_type == "member-discount"
            disc_benefit = split_tags.select{ |e| e.include?(purchase_type+VALUE_DELIMITER)}
          
            unless disc_benefit.empty?
              disc_benefit = disc_benefit[0].split(VALUE_DELIMITER)
              
              benefit_value = disc_benefit[1]
              benefit_value_type = disc_benefit[2]
          
              pricing_data = {program: program_name.downcase+'-active', 
                              value: benefit_value, 
                              type: benefit_value_type}
              product_pricing.push(pricing_data)
            end
          else
            puts 'no member price on product'
          end
  
        end
    
        product_map = (product_pricing.map{|x| x[:program.downcase]})
        
        matched_tag = []
        customer_map = []
        
        if !anon_customer
          customer_map = customer_tags.map { |tag| tag.downcase.strip }
        elsif anon_tag.length > 0
          customer_map = [anon_tag]
        end   
        matched_tag = CustomerTagSelector.new().tag_match(product_map, customer_map)  
          
        next unless !matched_tag.empty?
          benefit_data = product_pricing.find {|x| x[:program] == matched_tag[0]}

          if not benefit_data.nil?
            ###########################################
            # Apply the product discount to the line_item
            ###########################################
            discount_applicator = MembershipDiscountor.new(benefit_data[:type], benefit_data[:value].to_f)
            discount_applicator.apply(line_item)
            
            chomp_tag =  matched_tag[0].chomp! '-active'

            ###########################################
            # The below hash is REQUIRED for applying recurring subscription discounts.
            # Recurring discounts must have the line_item_properties added for Recharge processing.
            # Removing this or failing to add the properties will result in the purchase being handled as 
            # a one-time purchase and subsequent recurring orders will not have any member discount applied 
            ###########################################
            reduction_hash = { "_membership_program_tag" => chomp_tag,
                              "_membership_reduction_type" => benefit_data[:type], 
                              "_membership_reduction_value" => benefit_data[:value],
                              "_membership_reduction_benefit" => full_benefit}
            line_item.change_properties(reduction_hash, { message: 'membership price adjustment' }) 
            ###########################################
          end 
        end
      end
    end
  end

discounter = DiscountForMember.new()
discounter.run(Input.cart)

Output.cart = Input.cart

Test the discounting functionality

Because this is custom functionality, it's important to test thoroughly to ensure it works as expected. We recommend thoroughly navigating your site and completing test checkouts as both a non-member and logged-in test member to ensure the discounts are applied correctly.


Need Help? Contact Us