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.
- Go to Sales channels > Online Store > Themes and click customize on the duplicate copy of your current theme.
- Click the ... button in the top navigation bar and select Edit code.
- Go to Snippets > price.liquid
- 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>
- 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> <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"> {{ 'accessibility.unit_price_separator' | t }} </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> <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>
- 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).
- Install the Shopify Script Editor if you haven’t already. Refer to Shopify's documentation for more information on how to use the app.
- In the Script Editor app, click Create Script > Line Items > Blank template and click Create script.
- Set a descriptive title and click the Code tab
- 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.
Updated about 1 year ago