' . implode( '', $parts ) . '
';
}
/**
* Plain text inside a field group.
*
* @param string $text Text.
* @return string
*/
public static function text( $text ) {
return ' via wp_kses).
* @param string $example Example line (escaped HTML allowed: strong, a).
*/
private static function explain_box( $title, array $steps, $example = '' ) {
$allowed = array(
'strong' => array(),
'a' => array( 'href' => array() ),
'em' => array(),
'code' => array(),
);
echo '' . esc_html( $title ) . '
';
foreach ( $steps as $step ) {
echo '- ' . wp_kses( $step, $allowed ) . '
';
}
echo '
';
if ( $example ) {
echo '' . esc_html__( 'Example with your current settings', 'loyaltyspin' ) . '' . wp_kses( $example, $allowed ) . '
';
}
echo ' ';
}
/**
* Money as plain text.
*
* @param float $v Value.
* @return string
*/
private static function money( $v ) {
return html_entity_decode( wp_strip_all_tags( wc_price( $v ) ), ENT_QUOTES, 'UTF-8' );
}
/**
* Text/number input.
*
* @param string $name Name.
* @param mixed $value Value.
* @param string $type Type.
* @param string $extra Extra attributes.
* @return string
*/
private static function input( $name, $value, $type = 'text', $extra = '' ) {
$class = 'number' === $type ? 'small-text' : ( 'date' === $type ? 'lp-date' : 'regular-text' );
$attr = false === strpos( $extra, 'class=' ) ? ' class="' . $class . '"' : '';
return '';
}
/**
* Checkbox.
*
* @param string $name Name.
* @param mixed $checked Checked.
* @param string $label Label.
* @param string $tip Bubble text.
* @return string
*/
private static function check( $name, $checked, $label, $tip = '' ) {
return '' . self::tip( $tip );
}
/* ---------- Explainers ---------- */
/**
* General.
*
* @param array $s Settings.
*/
private static function explain_general( $s ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found -- uniform explainer signature.
$items = array(
__( 'Coins: customers collect your own branded coins on every confirmed order and spend them at checkout as a discount.', 'loyaltyspin' ),
__( 'Tiers: customers who spend more in total move up a level (e.g. Silver, Gold) and get a % off every order automatically.', 'loyaltyspin' ),
);
if ( Settings::has_pro_code() ) {
$items[] = __( 'Gifts: a free product is added to the cart when the cart reaches an amount you choose.', 'loyaltyspin' );
$items[] = __( 'Spin & Win: customers earn spins by ordering and win coins or coupons on a prize wheel.', 'loyaltyspin' );
$items[] = __( 'Lucky Draw: when your store hits a target (e.g. 1,000 orders), winners are picked at random from qualifying orders.', 'loyaltyspin' );
$items[] = __( 'Everything is triggered only by orders in a processed status. If an order is cancelled or refunded, its coins, spins and draw entries are taken back automatically.', 'loyaltyspin' );
} else {
$items[] = __( 'Coins are added only for orders in a processed status. If an order is cancelled or refunded, its coins are taken back automatically.', 'loyaltyspin' );
}
self::explain_box( __( 'How LoyaltySpin works', 'loyaltyspin' ), $items );
}
/**
* Coins.
*
* @param array $s Settings.
*/
private static function explain_coins( $s ) {
$c = $s['coins'];
$order = 1000;
$coins = \LoyaltyPress\Coins\Earn::coins_for_amount( $order );
$value = \LoyaltyPress\Coins\Redeem::value_of( $coins );
$back = $order > 0 ? round( $value / $order * 100, 2 ) : 0;
$max = $order * (float) $c['max_redeem_percent'] / 100;
$plural = $c['plural'];
self::explain_box(
__( 'How coins work', 'loyaltyspin' ),
array(
/* translators: %s: coin name */
sprintf( __( 'A customer places an order. When it reaches a processed status (Processing/Completed), %s are added to their balance automatically.', 'loyaltyspin' ), esc_html( $plural ) ),
__( 'Coins are calculated on item prices after discounts. Shipping, tax and free gifts never earn coins. You can override the rate per product (Products → Edit → Product data → General).', 'loyaltyspin' ),
__( 'Customers see how many coins they will earn on the product page, in the cart, at checkout, on the thank-you page and in order emails.', 'loyaltyspin' ),
__( 'At checkout a "Use coins" box lets them turn coins into a discount, within your minimum and maximum limits.', 'loyaltyspin' ),
__( 'Cancelled or refunded orders take back the coins they earned and give back the coins that were used on them.', 'loyaltyspin' ),
),
sprintf(
/* translators: 1: order value, 2: coins, 3: coin name, 4: value, 5: percent, 6: max discount */
__( 'A %1$s order earns %2$s %3$s, worth %4$s on a future order (about %5$s%% back). On a %1$s cart, a customer can use coins for up to %6$s off.', 'loyaltyspin' ),
esc_html( self::money( $order ) ),
esc_html( number_format_i18n( $coins ) ),
esc_html( $plural ),
esc_html( self::money( $value ) ),
esc_html( wc_format_localized_decimal( $back ) ),
esc_html( self::money( $max ) )
)
);
}
/**
* Tiers.
*
* @param array $s Settings.
*/
private static function explain_tiers( $s ) {
$levels = \LoyaltyPress\Discounts\Tiers::levels();
$ex = null;
foreach ( $levels as $l ) {
if ( (float) $l['percent'] > 0 ) {
$ex = $l;
break;
}
}
$ladder = array();
foreach ( $levels as $l ) {
/* translators: 1: tier, 2: amount, 3: percent */
$ladder[] = sprintf( __( '%1$s from %2$s (%3$s%% off)', 'loyaltyspin' ), $l['name'], self::money( $l['threshold'] ), wc_format_localized_decimal( $l['percent'] ) );
}
$example = '';
if ( $ex ) {
$cart = 2000;
$discount = $cart * (float) $ex['percent'] / 100;
$cap = (float) $s['tiers']['cap_amount'];
if ( $cap > 0 ) {
$discount = min( $discount, $cap );
}
$coins = \LoyaltyPress\Coins\Earn::coins_for_amount( $cart - $discount );
$example = sprintf(
/* translators: 1: tier, 2: threshold, 3: cart, 4: percent, 5: discount, 6: pays, 7: coins, 8: coin name */
__( 'A customer whose paid orders add up to %2$s or more is %1$s. They add %3$s of products to the cart: a line "%1$s member discount (%4$s%%)" of -%5$s appears automatically, they pay %6$s and earn %7$s %8$s on that amount.', 'loyaltyspin' ),
esc_html( $ex['name'] ),
esc_html( self::money( $ex['threshold'] ) ),
esc_html( self::money( $cart ) ),
esc_html( wc_format_localized_decimal( $ex['percent'] ) ),
esc_html( self::money( $discount ) ),
esc_html( self::money( $cart - $discount ) ),
esc_html( number_format_i18n( $coins ) ),
esc_html( $s['coins']['plural'] )
);
}
self::explain_box(
__( 'How tiers work', 'loyaltyspin' ),
array(
/* translators: %s: list of tiers */
$ladder ? sprintf( __( 'Your levels: %s.', 'loyaltyspin' ), esc_html( implode( ' → ', $ladder ) ) ) : __( 'Add your levels below.', 'loyaltyspin' ),
__( 'A customer\'s level is decided by lifetime spend: the total of all their paid (Processing/Completed) orders, as WooCommerce counts it. Guests have no level.', 'loyaltyspin' ),
__( 'Customers move up automatically the moment their total crosses the next threshold. Nothing to apply or approve.', 'loyaltyspin' ),
__( 'The level\'s % is taken off automatically in the cart as a line such as "Silver member discount (3%)". It is calculated on item prices after coupons; sale items and excluded categories are skipped; the cap limits the discount per order.', 'loyaltyspin' ),
__( 'Customers see their level and how much more they need to spend for the next one in the cart and on their My Rewards page.', 'loyaltyspin' ),
__( 'Coins are earned on the amount after the tier discount, so the two never double up.', 'loyaltyspin' ),
),
$example
);
}
/**
* Gifts.
*
* @param array $s Settings.
*/
private static function explain_gifts( $s ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found -- uniform explainer signature.
self::explain_box(
__( 'How gifts work', 'loyaltyspin' ),
array(
__( 'Pick (or create) the product to give away and the cart amount that unlocks it.', 'loyaltyspin' ),
__( 'When a cart reaches the amount, the gift is added automatically at 0, with quantity fixed at 1. If the cart drops below, the gift is removed.', 'loyaltyspin' ),
__( 'With several rules, the customer gets the gift of the highest amount their cart reaches (one gift per order).', 'loyaltyspin' ),
__( 'The gift is skipped when it is out of stock. Stock is reduced normally when the order is placed.', 'loyaltyspin' ),
__( 'Gift products you create here are hidden from your shop and can only be received as a gift.', 'loyaltyspin' ),
),
sprintf( /* translators: 1: amount threshold, 2: example cart amount */ __( 'Rule "Cart subtotal ≥ %1$s → Gift Pouch, limit 1 per customer": a customer with %2$s in the cart sees "Gift Pouch — Free gift" added; the second time they order, no gift is added.', 'loyaltyspin' ), self::cur() . '2,000', self::cur() . '2,150' )
);
}
/**
* Spin.
*
* @param array $s Settings.
*/
private static function explain_spin( $s ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found -- uniform explainer signature.
self::explain_box(
__( 'How Spin & Win works', 'loyaltyspin' ),
array(
__( 'Customers earn spins with confirmed orders (choose the rule below). Spins from a cancelled or refunded order are removed if unused.', 'loyaltyspin' ),
__( 'They spin in the popup (choose when it appears below), on My Account → My Rewards, from the thank-you page button, or on any page where you put the shortcode [loyaltypress_spin_wheel].', 'loyaltyspin' ),
/* translators: %s: link */
sprintf( __( 'Your server picks the prize at random using the weights you set on the Spin Wheel page. The animation only shows the result, so it cannot be cheated from the browser.', 'loyaltyspin' ), esc_url( admin_url( 'admin.php?page=loyaltypress-spin' ) ) ),
__( 'Coins are added instantly; coupons are single-use, locked to the customer\'s email and shown on screen and by email.', 'loyaltyspin' ),
__( 'Customers see the prizes and odds next to the wheel, so the game is transparent.', 'loyaltyspin' ),
),
esc_html( \LoyaltyPress\Frontend\Shortcodes::how_to_earn_spins() )
);
}
/**
* Appearance.
*
* @param array $s Settings.
*/
private static function explain_appearance( $s ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found -- uniform explainer signature.
self::explain_box(
__( 'How LoyaltySpin fits your design', 'loyaltyspin' ),
array(
__( 'Everything customers see uses your theme\'s fonts and sizes.', 'loyaltyspin' ),
__( 'With "Match my theme", buttons are your theme\'s own buttons, and boxes, borders and muted text are tinted from your theme\'s text colour, so they look right on light and dark designs.', 'loyaltyspin' ),
__( 'Works with block themes (e.g. Twenty Twenty-Five), classic themes (Astra, Storefront, Flatsome, Kadence…) and page builders, on the classic and block cart and checkout.', 'loyaltyspin' ),
__( 'Developers can fine-tune with CSS variables such as --lp-accent and --lp-radius, or copy templates/myaccount/rewards.php into the theme.', 'loyaltyspin' ),
)
);
}
/* ---------- Tabs ---------- */
/**
* General tab.
*
* @param array $v Values.
* @param array $s All settings.
*/
private static function tab_general( $v, $s ) {
$mods = array(
'coins' => array( __( 'Loyalty Coins', 'loyaltyspin' ), __( 'Customers earn coins on orders and spend them at checkout.', 'loyaltyspin' ) ),
'tiers' => array( __( 'Tier discounts', 'loyaltyspin' ), __( 'Automatic % off for customers who have spent more in total.', 'loyaltyspin' ) ),
'gifts' => array( __( 'Gifts on purchase', 'loyaltyspin' ), __( 'Add a free product when the cart reaches an amount.', 'loyaltyspin' ) ),
'spin' => array( __( 'Spin & Win', 'loyaltyspin' ), __( 'Prize wheel. Customers earn spins by ordering.', 'loyaltyspin' ) ),
'draw' => array( __( 'Lucky Draw', 'loyaltyspin' ), __( 'Pick random winners when your store hits a target.', 'loyaltyspin' ) ),
);
$html = '';
foreach ( $mods as $k => $m ) {
if ( in_array( $k, Settings::PRO_MODULES, true ) && ! Settings::is_pro() ) {
$html .= '' . esc_html( $m[0] ) . ' — ' . esc_html__( 'available in LoyaltySpin Pro', 'loyaltyspin' ) . ' ' . esc_html__( 'Get Pro', 'loyaltyspin' ) . '';
continue;
}
$html .= '' . self::check( 'modules[' . $k . ']', $s['modules'][ $k ] ?? 0, $m[0], $m[1] ) . '';
}
self::row( __( 'Modules', 'loyaltyspin' ), '' . $html . '
', __( 'Turn features on or off. A switched-off module does not load at all, so it costs nothing in speed.', 'loyaltyspin' ) );
$html = '';
foreach ( wc_get_order_statuses() as $key => $label ) {
$k = substr( $key, 3 );
if ( in_array( $k, self::FORBIDDEN_STATUSES, true ) ) {
continue;
}
$html .= ' ';
}
self::row( __( 'Processed order statuses', 'loyaltyspin' ), $html, Settings::is_pro() ? __( 'An order earns coins, spins and draw entries only when it reaches one of these statuses. Pending payment, On hold, Failed, Cancelled and Refunded can never be chosen, so unpaid orders never earn.', 'loyaltyspin' ) : __( 'An order earns coins only when it reaches one of these statuses. Pending payment, On hold, Failed, Cancelled and Refunded can never be chosen, so unpaid orders never earn.', 'loyaltyspin' ) );
if ( Settings::is_pro() ) {
self::row( __( 'Show odds', 'loyaltyspin' ), self::check( 'general[show_odds]', $v['show_odds'], __( 'Show prizes, odds and terms next to the wheel', 'loyaltyspin' ) ), __( 'Recommended. Showing the chance of each prize builds trust and is expected under consumer-protection rules.', 'loyaltyspin' ) );
self::row( __( 'Age notice', 'loyaltyspin' ), self::check( 'general[min_age_notice]', $v['min_age_notice'], __( 'Show "Open to customers aged 18 and over"', 'loyaltyspin' ) ), __( 'Adds an 18+ line under Spin & Win and Lucky Draw terms.', 'loyaltyspin' ) );
self::row( __( 'Excluded regions', 'loyaltyspin' ), self::input( 'general[excluded_states]', $v['excluded_states'], 'text', 'placeholder="IN:TN, IN:TG"' ), __( 'Spin & Win and Lucky Draw are switched off for customers whose billing address is in these places. Use a state code (TN), country:state (IN:TN) or a whole country (US:*), separated by commas. Coins and tiers still work there. Ask your lawyer which states to list.', 'loyaltyspin' ) );
}
self::row( __( 'Uninstall', 'loyaltyspin' ), self::check( 'general[delete_data]', $v['delete_data'], __( 'Delete all LoyaltySpin data when the plugin is deleted', 'loyaltyspin' ) ), __( 'Leave off unless you are sure. When off, deleting and reinstalling the plugin keeps every balance and history.', 'loyaltyspin' ) );
}
/**
* Coins tab.
*
* @param array $v Values.
*/
private static function tab_coins( $v ) {
$cur = self::cur();
self::row(
__( 'Coin name', 'loyaltyspin' ),
'' . self::mini( __( 'Singular', 'loyaltyspin' ), self::input( 'coins[singular]', $v['singular'], 'text', 'placeholder="Coin"' ) ) . self::mini( __( 'Plural', 'loyaltyspin' ), self::input( 'coins[plural]', $v['plural'], 'text', 'placeholder="Coins"' ) ) . self::mini( __( 'Icon (emoji)', 'loyaltyspin' ), self::input( 'coins[icon]', $v['icon'], 'text', 'style="width:70px"' ) ) . '
' . esc_html__( 'Or upload an image', 'loyaltyspin' ) . '' . self::media( 'coins[icon_id]', (int) ( $v['icon_id'] ?? 0 ) ) . '
',
__( 'What customers see everywhere, e.g. "Star Coin" / "Star Coins". For the icon, type an emoji (⭐ 🪙 💎) or upload your own coin image; an uploaded image is used instead of the emoji. Square PNG or SVG with a transparent background, at least 64×64, looks best.', 'loyaltyspin' )
);
self::row( __( 'Earn rate', 'loyaltyspin' ), self::fields( self::input( 'coins[earn_coins]', $v['earn_coins'], 'number', 'min="0"' ), self::text( __( 'coins for every', 'loyaltyspin' ) ), self::group( self::input( 'coins[earn_per_amount]', $v['earn_per_amount'], 'number', 'min="1" step="0.01"' ), $cur ), self::text( __( 'spent', 'loyaltyspin' ) ) ), __( 'How fast customers collect coins. Calculated on item prices after discounts, without shipping, tax or free gifts. Rounded down.', 'loyaltyspin' ) );
self::row( __( 'Redeem rate', 'loyaltyspin' ), self::fields( self::input( 'coins[redeem_coins]', $v['redeem_coins'], 'number', 'min="1"' ), self::text( __( 'coins =', 'loyaltyspin' ) ), self::group( self::input( 'coins[redeem_value]', $v['redeem_value'], 'number', 'min="0" step="0.01"' ), $cur ), self::text( __( 'off', 'loyaltyspin' ) ) ), __( 'What coins are worth when spent. Together with the earn rate this sets your real cashback; see the example above.', 'loyaltyspin' ) );
self::row( __( 'Redeem limits', 'loyaltyspin' ), self::fields( self::text( __( 'Minimum', 'loyaltyspin' ) ), self::input( 'coins[min_redeem]', $v['min_redeem'], 'number', 'min="0"' ), self::text( __( 'coins per order', 'loyaltyspin' ) ), self::sep(), self::text( __( 'Maximum', 'loyaltyspin' ) ), self::group( self::input( 'coins[max_redeem_percent]', $v['max_redeem_percent'], 'number', 'min="0" max="100"' ), '', '%' ), self::text( __( 'of the order', 'loyaltyspin' ) ) ), __( 'Minimum coins a customer must use at once, and the largest share of an order that coins can pay for. Protects your margin.', 'loyaltyspin' ) );
self::row( __( 'Expiry', 'loyaltyspin' ), self::fields( self::text( __( 'Coins expire after', 'loyaltyspin' ) ), self::group( self::input( 'coins[expiry_months]', $v['expiry_months'], 'number', 'min="0"' ), '', __( 'months', 'loyaltyspin' ) ), self::sep(), self::text( __( 'Remind', 'loyaltyspin' ) ), self::group( self::input( 'coins[reminder_days]', $v['reminder_days'], 'number', 'min="0"' ), '', __( 'days before', 'loyaltyspin' ) ) ), __( 'Coins expire this many months after they were earned (0 = never); the oldest coins are used first. Customers get a reminder email before coins expire, at most once a week.', 'loyaltyspin' ) );
self::row(
__( 'Bonus coins', 'loyaltyspin' ),
''
. self::mini( __( 'Sign-up', 'loyaltyspin' ), self::input( 'coins[bonus_signup]', $v['bonus_signup'], 'number', 'min="0"' ) )
. self::mini( __( 'First order', 'loyaltyspin' ), self::input( 'coins[bonus_first_order]', $v['bonus_first_order'], 'number', 'min="0"' ) )
. self::mini( __( 'Verified review', 'loyaltyspin' ), self::input( 'coins[bonus_review]', $v['bonus_review'], 'number', 'min="0"' ) )
. self::mini( __( 'Birthday', 'loyaltyspin' ), self::input( 'coins[bonus_birthday]', $v['bonus_birthday'], 'number', 'min="0"' ) )
. '
',
__( 'Extra coins for actions. 0 switches a bonus off. Review bonus: once per product, only for customers who bought it. Birthday: customers add the day and month in Account details, once.', 'loyaltyspin' )
);
self::row( __( 'Daily earn cap', 'loyaltyspin' ), self::fields( self::group( self::input( 'coins[daily_earn_cap]', $v['daily_earn_cap'], 'number', 'min="0"' ), '', __( 'coins per day', 'loyaltyspin' ) ) ), __( 'The most purchase coins one customer can earn per day. 0 = no cap. Useful against bulk-order abuse.', 'loyaltyspin' ) );
self::row(
__( 'Show earnings', 'loyaltyspin' ),
''
. self::check( 'coins[show_on_product]', $v['show_on_product'] ?? 1, __( 'On product pages ("Earn 120 Coins")', 'loyaltyspin' ) )
. self::check( 'coins[show_on_loop]', $v['show_on_loop'] ?? 1, __( 'On shop and category pages (small badge)', 'loyaltyspin' ) )
. '
',
__( 'Cart, mini cart, checkout, thank-you page and order emails always show the coins for the order. With page builders (Elementor, Divi…) the messages are placed automatically; to choose the spot yourself, add the shortcode [loyaltypress_product_coins] to a product template or [loyaltypress_cart_coins] to a cart page.', 'loyaltyspin' )
);
}
/**
* Tiers tab.
*
* @param array $v Values.
*/
private static function tab_tiers( $v ) {
$levels = array_pad(
array_values( (array) $v['levels'] ),
6,
array(
'name' => '',
'threshold' => '',
'percent' => '',
)
);
$cur = self::cur();
$html = '| ' . esc_html__( 'Level name', 'loyaltyspin' ) . ' | ' . esc_html__( 'Lifetime spend from', 'loyaltyspin' ) . self::tip( __( 'Total of the customer\'s paid orders needed to reach this level. The first level is usually 0.', 'loyaltyspin' ) ) . ' | ' . esc_html__( '% off every order', 'loyaltyspin' ) . ' |
';
foreach ( $levels as $i => $l ) {
$html .= '| ' . self::input( "tiers[levels][$i][name]", $l['name'], 'text', 'placeholder="' . esc_attr__( 'Add a level…', 'loyaltyspin' ) . '"' ) . ' | ' . self::group( self::input( "tiers[levels][$i][threshold]", $l['threshold'], 'number', 'min="0" step="0.01" class="lp-num-wide"' ), $cur ) . ' | ' . self::group( self::input( "tiers[levels][$i][percent]", $l['percent'], 'number', 'min="0" max="100" step="0.01"' ), '', '%' ) . ' |
';
}
$html .= '
';
self::row_wide( __( 'Levels', 'loyaltyspin' ), $html, __( 'Up to 6 levels, any names. Leave a name empty to remove that row. Order does not matter; they are sorted by amount.', 'loyaltyspin' ) );
self::row( __( 'Cap per order', 'loyaltyspin' ), self::fields( self::group( self::input( 'tiers[cap_amount]', $v['cap_amount'], 'number', 'min="0" step="0.01" class="lp-num-wide"' ), $cur ) ), __( 'The most a tier discount can take off one order. Stops a very large order from getting a huge discount. 0 = no cap.', 'loyaltyspin' ) );
self::row(
__( 'Rules', 'loyaltyspin' ),
'' . self::check( 'tiers[exclude_sale]', $v['exclude_sale'], __( 'Exclude sale items', 'loyaltyspin' ), __( 'Products that already have a sale price get no extra tier discount.', 'loyaltyspin' ) ) . ''
. self::check( 'tiers[stack_coupons]', $v['stack_coupons'], __( 'Allow together with other coupons', 'loyaltyspin' ), __( 'When off, the tier discount is skipped if the customer uses a coupon.', 'loyaltyspin' ) ) . '
'
);
$cats = get_terms(
array(
'taxonomy' => 'product_cat',
'hide_empty' => false,
'number' => 300,
)
);
$html = '';
foreach ( is_array( $cats ) ? $cats : array() as $cat ) {
$html .= '';
}
$html .= '
';
self::row( __( 'Excluded categories', 'loyaltyspin' ), $html, __( 'Products in these categories never get a tier discount (e.g. gift cards, already-discounted bundles).', 'loyaltyspin' ) );
}
/**
* One gift rule row.
*
* @param string|int $i Index (or __i__ for the template).
* @param array $r Rule.
* @return string
*/
private static function gift_row( $i, array $r ) {
$r = wp_parse_args(
$r,
array(
'id' => '',
'active' => 1,
'threshold' => '',
'product_id' => '',
'start' => '',
'end' => '',
'per_customer' => 0,
)
);
$product = $r['product_id'] ? wc_get_product( (int) $r['product_id'] ) : null;
$option = $product ? '' : '';
$tpl = '__i__' === $i;
return ''
. ' | '
. '' . self::group( self::input( "gifts[rules][$i][threshold]", $r['threshold'], 'number', 'min="0" step="0.01" class="lp-num-wide" placeholder="2000"' ), self::cur() ) . ' | '
. ' | '
. '' . self::input( "gifts[rules][$i][start]", $r['start'], 'date' ) . ' | '
. '' . self::input( "gifts[rules][$i][end]", $r['end'], 'date' ) . ' | '
. '' . self::input( "gifts[rules][$i][per_customer]", $r['per_customer'], 'number', 'min="0"' ) . ' | '
. ' | '
. '
';
}
/**
* Gifts tab.
*
* @param array $v Values.
*/
private static function tab_gifts( $v ) {
$rules = array_values( (array) $v['rules'] );
if ( ! $rules ) {
$rules[] = array();
}
$html = ''
. '| ' . esc_html__( 'On', 'loyaltyspin' ) . ' | '
. '' . esc_html__( 'Cart subtotal ≥', 'loyaltyspin' ) . self::tip( __( 'The cart value (paid items only) that unlocks this gift.', 'loyaltyspin' ) ) . ' | '
. '' . esc_html__( 'Gift product', 'loyaltyspin' ) . self::tip( __( 'Start typing a product name. Or click "Create gift product" below to make a new one that is hidden from your shop.', 'loyaltyspin' ) ) . ' | '
. '' . esc_html__( 'Starts', 'loyaltyspin' ) . self::tip( __( 'Optional. Leave empty to start now.', 'loyaltyspin' ) ) . ' | '
. '' . esc_html__( 'Ends', 'loyaltyspin' ) . self::tip( __( 'Optional. Leave empty to run until you switch it off.', 'loyaltyspin' ) ) . ' | '
. '' . esc_html__( 'Per customer', 'loyaltyspin' ) . self::tip( __( 'How many times one customer can get this gift. 0 = unlimited.', 'loyaltyspin' ) ) . ' | '
. ' |
';
foreach ( $rules as $i => $r ) {
$html .= self::gift_row( $i, $r );
}
$html .= '' . self::gift_row( '__i__', array() ) . '
';
$html .= '
';
$html .= self::new_gift_dialog();
$html = self::label_controls( $html, __( 'Gift rule', 'loyaltyspin' ) );
\LoyaltyPress\Html::out( '' . esc_html__( 'Gift rules', 'loyaltyspin' ) . ' ' . $html . ' |
' );
}
/**
* Inline "create gift product" form.
*
* @return string
*/
private static function new_gift_dialog() {
return ''
. '
' . esc_html__( 'New gift product', 'loyaltyspin' ) . '
'
. '
' . esc_html__( 'Creates a normal WooCommerce product that is hidden from your shop and search, so it can only be received as a gift.', 'loyaltyspin' ) . '
'
. '
'
. '
'
. '
';
}
/**
* Spin tab.
*
* @param array $v Values.
*/
private static function tab_spin( $v ) {
$modes = array(
'orders' => array(
'label' => __( 'Every few orders', 'loyaltyspin' ),
'pre' => __( 'Every', 'loyaltyspin' ),
'post' => __( 'confirmed orders', 'loyaltyspin' ),
/* translators: 1: N, 2: 2N, 3: 3N */
'example' => __( 'Example: a spin on order %1$s, %2$s, %3$s and so on.', 'loyaltyspin' ),
),
'spend' => array(
'label' => __( 'For every amount spent', 'loyaltyspin' ),
'pre' => __( 'One spin for every', 'loyaltyspin' ),
'post' => __( 'spent in total', 'loyaltyspin' ),
/* translators: 1: amount, 2: 1.5x amount, 3: half the amount, 4: twice the amount */
'example' => __( 'Example: a customer who has spent %2$s in total has 1 spin, and the extra %3$s counts towards the next spin at %4$s.', 'loyaltyspin' ),
),
'milestone' => array(
'label' => __( 'Once, after a number of orders', 'loyaltyspin' ),
'pre' => __( 'One spin after', 'loyaltyspin' ),
'post' => __( 'orders (once per customer)', 'loyaltyspin' ),
/* translators: 1: N */
'example' => __( 'Example: one spin on the customer\'s order number %1$s, then no more spins from orders.', 'loyaltyspin' ),
),
);
$mode = isset( $modes[ $v['trigger'] ] ) ? $v['trigger'] : 'orders';
$sel = '';
$value = 'spend' === $mode ? (float) $v['trigger_value'] : max( 1, (int) round( (float) $v['trigger_value'] ) );
$num = '';
$group = '' . esc_html( self::cur() ) . '' . $num . '';
$data = wp_json_encode(
array(
'modes' => $modes,
'cur' => self::cur(),
)
);
$html = ''
. self::fields( $sel )
. '
' . esc_html( $modes[ $mode ]['pre'] ) . '' . $group . '' . esc_html( $modes[ $mode ]['post'] ) . '
'
. '
';
self::row( __( 'Earn a spin', 'loyaltyspin' ), $html, __( 'Choose how customers earn spins. Only confirmed (Processing/Completed) orders count, and spins from cancelled or refunded orders are taken back if unused.', 'loyaltyspin' ) );
self::row( __( 'Spins expire after', 'loyaltyspin' ), self::fields( self::group( self::input( 'spin[token_expiry_days]', $v['token_expiry_days'], 'number', 'min="0"' ), '', __( 'days', 'loyaltyspin' ) ), self::text( __( '0 = never', 'loyaltyspin' ) ) ), __( 'Unused spins disappear after this many days. Creates urgency to come back.', 'loyaltyspin' ) );
self::row( __( 'Prize coupons expire after', 'loyaltyspin' ), self::fields( self::group( self::input( 'spin[coupon_expiry_days]', $v['coupon_expiry_days'], 'number', 'min="0"' ), '', __( 'days', 'loyaltyspin' ) ) ), __( 'Coupons won on the wheel stop working after this many days.', 'loyaltyspin' ) );
self::row( __( 'Rate limit', 'loyaltyspin' ), self::fields( self::group( self::input( 'spin[max_spins_per_hour]', $v['max_spins_per_hour'], 'number', 'min="1"' ), '', __( 'spins per hour', 'loyaltyspin' ) ) ), __( 'Safety limit against scripts or double-clicks. Normal customers never hit it.', 'loyaltyspin' ) );
self::row( __( 'Terms shown to customers', 'loyaltyspin' ), '', __( 'Shown under the wheel in "Prizes, odds & terms". Keep it short and clear.', 'loyaltyspin' ) );
self::row( __( 'Prizes', 'loyaltyspin' ), '' . esc_html__( 'Edit wheel prizes & preview', 'loyaltyspin' ) . '', __( 'Set the slices, their chance of winning, images and stock limits, see a live preview and test the odds.', 'loyaltyspin' ) );
self::row( __( 'Centre logo', 'loyaltyspin' ), self::media( 'spin[logo_id]', (int) ( $v['logo_id'] ?? 0 ) ), __( 'Optional. Your logo replaces the word SPIN in the middle of the wheel. A square or round PNG works best.', 'loyaltyspin' ) );
$p = wp_parse_args( (array) ( $v['popup'] ?? array() ), Settings::defaults()['spin']['popup'] );
self::section( __( 'Popup', 'loyaltyspin' ), __( 'Bring the wheel to customers instead of waiting for them to open My Rewards. The popup shows the same wheel, so they can spin without leaving the page.', 'loyaltyspin' ) );
self::row( __( 'Show popup', 'loyaltyspin' ), self::check( 'spin[popup][enabled]', $p['enabled'], __( 'Show the Spin & Win popup on the storefront', 'loyaltyspin' ) ), __( 'Master switch for the popup. The wheel on My Rewards and the [loyaltypress_spin_wheel] shortcode work either way.', 'loyaltyspin' ) );
$triggers = array(
'login' => array( __( 'Right after the customer logs in', 'loyaltyspin' ), __( 'Shown once on the first page after logging in. Great for returning customers with unused spins.', 'loyaltyspin' ) ),
'thankyou' => array( __( 'On the order thank-you page', 'loyaltyspin' ), __( 'Recommended. Customers just earned a spin with this order, so it is the happiest moment to spin.', 'loyaltyspin' ) ),
'cart' => array( __( 'On the cart page', 'loyaltyspin' ), __( 'A win here (e.g. a coupon) can be used straight away on the current order.', 'loyaltyspin' ) ),
'checkout' => array( __( 'On the checkout page', 'loyaltyspin' ), __( 'Use with care: a popup during payment can distract customers. Best with "Only customers who have a spin".', 'loyaltyspin' ) ),
'account' => array( __( 'On My Account pages', 'loyaltyspin' ), __( 'When customers check orders or details. Not shown on My Rewards, which already has the wheel.', 'loyaltyspin' ) ),
'delay' => array( __( 'On any page, after a delay', 'loyaltyspin' ), __( 'Opens after the number of seconds set below on any storefront page.', 'loyaltyspin' ) ),
'scroll' => array( __( 'On any page, after scrolling', 'loyaltyspin' ), __( 'Opens when the visitor has scrolled the percentage of the page set below.', 'loyaltyspin' ) ),
'exit' => array( __( 'When leaving the page (exit intent)', 'loyaltyspin' ), __( 'Desktop only: opens when the mouse moves towards the browser tabs or close button.', 'loyaltyspin' ) ),
);
$html = '';
foreach ( $triggers as $k => $t ) {
$html .= '' . self::tip( $t[1] ) . '';
}
self::row( __( 'When to show it', 'loyaltyspin' ), '' . $html . '
', __( 'Tick one or more moments. The popup opens at the first one that happens.', 'loyaltyspin' ) );
self::row(
__( 'Delay and scroll', 'loyaltyspin' ),
self::fields( self::text( __( 'Delay', 'loyaltyspin' ) ), self::group( self::input( 'spin[popup][delay]', $p['delay'], 'number', 'min="1" max="300"' ), '', __( 'seconds', 'loyaltyspin' ) ), self::sep(), self::text( __( 'Scroll', 'loyaltyspin' ) ), self::group( self::input( 'spin[popup][scroll]', $p['scroll'], 'number', 'min="10" max="100"' ), '', __( '% of page', 'loyaltyspin' ) ) ),
__( 'Used by "after a delay" and "after scrolling".', 'loyaltyspin' )
);
self::row(
__( 'Who sees it', 'loyaltyspin' ),
self::radios(
'spin[popup][audience]',
$p['audience'],
array(
'has_spins' => __( 'Only customers who have a spin to use', 'loyaltyspin' ),
'everyone' => __( 'Everyone (others see how to earn a spin)', 'loyaltyspin' ),
)
),
__( 'Recommended: only customers with a spin, so nobody sees a wheel they cannot turn. "Everyone" works as a teaser: guests are invited to log in and others see how to earn a spin.', 'loyaltyspin' )
);
self::row(
__( 'How often', 'loyaltyspin' ),
self::radios(
'spin[popup][frequency]',
$p['frequency'],
array(
'session' => __( 'Once per visit', 'loyaltyspin' ),
'day' => __( 'Once a day', 'loyaltyspin' ),
'always' => __( 'Every time a trigger happens', 'loyaltyspin' ),
)
),
__( 'Limits how often the popup returns after being closed. The login and thank-you triggers always show once per login or order.', 'loyaltyspin' )
);
self::row(
__( 'Devices', 'loyaltyspin' ),
self::radios(
'spin[popup][devices]',
$p['devices'],
array(
'all' => __( 'All', 'loyaltyspin' ),
'desktop' => __( 'Desktop only', 'loyaltyspin' ),
'mobile' => __( 'Mobile only', 'loyaltyspin' ),
)
),
__( 'On phones the popup fills the width of the screen and the wheel shrinks to fit.', 'loyaltyspin' )
);
}
/**
* Appearance tab.
*
* @param array $v Values.
*/
private static function tab_appearance( $v ) {
$v = wp_parse_args( (array) $v, Settings::defaults()['appearance'] );
self::row(
__( 'Style', 'loyaltyspin' ),
self::radios(
'appearance[style]',
$v['style'],
array(
'auto' => __( 'Match my theme (recommended)', 'loyaltyspin' ),
'custom' => __( 'Use my own colour', 'loyaltyspin' ),
)
),
__( 'Match my theme: buttons use your theme\'s own button style, and text, borders and backgrounds follow your theme\'s colours (light or dark). Use my own colour: LoyaltySpin buttons and highlights use the colour below.', 'loyaltyspin' )
);
self::row( __( 'Colour', 'loyaltyspin' ), '', __( 'Used for buttons, progress bars and links when "Use my own colour" is selected. Pick your brand colour.', 'loyaltyspin' ) );
self::row(
__( 'Corners', 'loyaltyspin' ),
self::radios(
'appearance[corners]',
$v['corners'],
array(
'square' => __( 'Square', 'loyaltyspin' ),
'rounded' => __( 'Rounded', 'loyaltyspin' ),
'pill' => __( 'Extra round', 'loyaltyspin' ),
)
),
__( 'Match the corner style of your theme\'s cards and buttons.', 'loyaltyspin' )
);
}
/**
* Emails tab.
*
* @param array $v Values.
*/
private static function tab_emails( $v ) {
$list = array(
'coins_earned' => array( __( 'Coins earned on an order', 'loyaltyspin' ), __( 'Sent when an order is confirmed and coins are added.', 'loyaltyspin' ) ),
'expiry_warning' => array( __( 'Coins about to expire', 'loyaltyspin' ), __( 'Sent before coins expire, at most once a week per customer.', 'loyaltyspin' ) ),
'spin_available' => array( __( 'New spin unlocked', 'loyaltyspin' ), __( 'Brings customers back to spin the wheel.', 'loyaltyspin' ) ),
'prize_won' => array( __( 'Spin prize won', 'loyaltyspin' ), __( 'Includes the coupon code, so it is not lost.', 'loyaltyspin' ) ),
'draw_winner' => array( __( 'Lucky Draw winner', 'loyaltyspin' ), __( 'Tells the winner their rank and prize.', 'loyaltyspin' ) ),
);
if ( ! Settings::has_pro_code() ) {
unset( $list['spin_available'], $list['prize_won'], $list['draw_winner'] );
}
$html = '';
foreach ( $list as $k => $m ) {
$html .= '' . self::check( "emails[$k]", $v[ $k ] ?? 0, $m[0], $m[1] ) . '';
}
self::row( __( 'Send to customers', 'loyaltyspin' ), '' . $html . '
', __( 'Emails use your WooCommerce email header, footer and colours (WooCommerce → Settings → Emails). Order emails also show the coins for that order.', 'loyaltyspin' ) );
}
/**
* Sanitize popup settings.
*
* @param array $p Raw.
* @return array
*/
private static function clean_popup( array $p ) {
$allowed = array( 'login', 'thankyou', 'cart', 'checkout', 'account', 'delay', 'scroll', 'exit' );
$aud = sanitize_key( $p['audience'] ?? 'has_spins' );
$freq = sanitize_key( $p['frequency'] ?? 'session' );
$dev = sanitize_key( $p['devices'] ?? 'all' );
return array(
'enabled' => empty( $p['enabled'] ) ? 0 : 1,
'triggers' => array_values( array_intersect( array_map( 'sanitize_key', (array) ( $p['triggers'] ?? array() ) ), $allowed ) ),
'delay' => min( 300, max( 1, absint( $p['delay'] ?? 8 ) ) ),
'scroll' => min( 100, max( 10, absint( $p['scroll'] ?? 50 ) ) ),
'audience' => in_array( $aud, array( 'has_spins', 'everyone' ), true ) ? $aud : 'has_spins',
'frequency' => in_array( $freq, array( 'session', 'day', 'always' ), true ) ? $freq : 'session',
'devices' => in_array( $dev, array( 'all', 'desktop', 'mobile' ), true ) ? $dev : 'all',
);
}
/* ---------- Gift product AJAX ---------- */
/**
* Create a hidden gift product.
*/
public static function ajax_new_gift() {
check_ajax_referer( 'loyaltypress_new_gift', 'nonce' );
if ( ! current_user_can( 'edit_products' ) || ! current_user_can( Admin::CAP ) ) { // phpcs:ignore WordPress.WP.Capabilities.Unknown -- WooCommerce capability.
wp_send_json_error( array( 'message' => __( 'You do not have permission to create products.', 'loyaltyspin' ) ), 403 );
}
$name = isset( $_POST['name'] ) ? sanitize_text_field( wp_unslash( $_POST['name'] ) ) : '';
$price = isset( $_POST['price'] ) ? wc_format_decimal( sanitize_text_field( wp_unslash( $_POST['price'] ) ) ) : '';
$stock = isset( $_POST['stock'] ) && '' !== $_POST['stock'] ? absint( $_POST['stock'] ) : null;
$image = isset( $_POST['image'] ) ? absint( $_POST['image'] ) : 0;
if ( '' === $name ) {
wp_send_json_error( array( 'message' => __( 'Please enter a name.', 'loyaltyspin' ) ) );
}
$p = new \WC_Product_Simple();
$p->set_name( $name );
$p->set_status( 'publish' );
$p->set_catalog_visibility( 'hidden' );
$p->set_regular_price( '' !== $price ? $price : '0' );
$p->set_sold_individually( true );
$p->set_reviews_allowed( false );
if ( null !== $stock ) {
$p->set_manage_stock( true );
$p->set_stock_quantity( $stock );
}
if ( $image && wp_attachment_is_image( $image ) ) {
$p->set_image_id( $image );
}
$p->update_meta_data( '_loyaltypress_gift_only', 1 );
$p->update_meta_data( '_loyaltypress_coins_mode', 'none' );
$id = $p->save();
Audit::log( 'gift_product_created', 'product:' . $id );
wp_send_json_success(
array(
'id' => $id,
'text' => wp_strip_all_tags( $p->get_formatted_name() ),
)
);
}
/* ---------- Save ---------- */
/**
* Save handler.
*/
public static function save() {
Admin::require_cap();
check_admin_referer( 'loyaltypress_save_settings' );
$tab = isset( $_POST['tab'] ) ? sanitize_key( wp_unslash( $_POST['tab'] ) ) : '';
$url = admin_url( 'admin.php?page=loyaltypress-settings&tab=' . $tab );
$d = Settings::defaults();
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized field by field below.
$in = isset( $_POST[ $tab ] ) ? wp_unslash( (array) $_POST[ $tab ] ) : array();
switch ( $tab ) {
case 'general':
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$mods = isset( $_POST['modules'] ) ? wp_unslash( (array) $_POST['modules'] ) : array();
$m = array();
foreach ( array_keys( $d['modules'] ) as $k ) {
$m[ $k ] = empty( $mods[ $k ] ) ? 0 : 1;
if ( ! Settings::is_pro() && in_array( $k, Settings::PRO_MODULES, true ) ) {
$m[ $k ] = (int) Settings::get( 'modules.' . $k ); // Not shown in the free edition: keep as is.
}
}
Settings::save_section( 'modules', $m );
$statuses = array_values( array_diff( array_map( 'sanitize_key', (array) ( $in['qualifying_statuses'] ?? array() ) ), self::FORBIDDEN_STATUSES ) );
$general = array(
'qualifying_statuses' => $statuses ? $statuses : array( 'processing', 'completed' ),
'show_odds' => empty( $in['show_odds'] ) ? 0 : 1,
'min_age_notice' => empty( $in['min_age_notice'] ) ? 0 : 1,
'excluded_states' => strtoupper( sanitize_text_field( $in['excluded_states'] ?? '' ) ),
'delete_data' => empty( $in['delete_data'] ) ? 0 : 1,
);
if ( ! Settings::is_pro() ) { // These fields are not shown in the free edition: keep stored values.
foreach ( array( 'show_odds', 'min_age_notice', 'excluded_states' ) as $k ) {
$general[ $k ] = Settings::get( 'general.' . $k );
}
}
Settings::save_section( 'general', $general );
break;
case 'coins':
$out = array();
foreach ( $d['coins'] as $k => $default ) {
if ( is_string( $default ) ) {
$out[ $k ] = sanitize_text_field( $in[ $k ] ?? $default );
} else {
$out[ $k ] = max( 0, (float) ( $in[ $k ] ?? 0 ) );
if ( is_int( $default ) ) {
$out[ $k ] = (int) $out[ $k ];
}
}
}
$out['singular'] = '' !== $out['singular'] ? $out['singular'] : 'Coin';
$out['plural'] = '' !== $out['plural'] ? $out['plural'] : 'Coins';
$out['earn_per_amount'] = max( 1, (int) $out['earn_per_amount'] );
$out['redeem_coins'] = max( 1, (int) $out['redeem_coins'] );
$out['redeem_value'] = round( max( 0, (float) ( $in['redeem_value'] ?? 0 ) ), 2 );
$out['max_redeem_percent'] = min( 100, $out['max_redeem_percent'] );
$out['icon_id'] = ( $out['icon_id'] && wp_attachment_is_image( $out['icon_id'] ) ) ? (int) $out['icon_id'] : 0;
Settings::save_section( 'coins', $out );
break;
case 'tiers':
$levels = array();
foreach ( (array) ( $in['levels'] ?? array() ) as $l ) {
$name = sanitize_text_field( $l['name'] ?? '' );
if ( '' === $name ) {
continue;
}
$levels[] = array(
'name' => $name,
'threshold' => max( 0, (float) ( $l['threshold'] ?? 0 ) ),
'percent' => min( 100, max( 0, (float) ( $l['percent'] ?? 0 ) ) ),
);
}
Settings::save_section(
'tiers',
array(
'levels' => $levels,
'cap_amount' => max( 0, (float) ( $in['cap_amount'] ?? 0 ) ),
'exclude_sale' => empty( $in['exclude_sale'] ) ? 0 : 1,
'exclude_cats' => array_map( 'absint', (array) ( $in['exclude_cats'] ?? array() ) ),
'stack_coupons' => empty( $in['stack_coupons'] ) ? 0 : 1,
)
);
break;
case 'gifts':
$rules = array();
foreach ( (array) ( $in['rules'] ?? array() ) as $key => $r ) {
$pid = absint( $r['product_id'] ?? 0 );
if ( '__i__' === (string) $key ) {
continue;
}
if ( ! $pid ) {
continue;
}
$date = static function ( $x ) {
$x = sanitize_text_field( (string) $x );
return preg_match( '/^\d{4}-\d{2}-\d{2}$/', $x ) ? $x : '';
};
$rules[] = array(
'id' => ! empty( $r['id'] ) ? sanitize_key( $r['id'] ) : 'g' . strtolower( wp_generate_password( 8, false ) ),
'active' => empty( $r['active'] ) ? 0 : 1,
'threshold' => max( 0, (float) ( $r['threshold'] ?? 0 ) ),
'product_id' => $pid,
'start' => $date( $r['start'] ?? '' ),
'end' => $date( $r['end'] ?? '' ),
'per_customer' => absint( $r['per_customer'] ?? 0 ),
);
}
Settings::save_section( 'gifts', array( 'rules' => $rules ) );
break;
case 'spin':
$trigger = sanitize_key( $in['trigger'] ?? 'orders' );
Settings::save_section(
'spin',
array(
'trigger' => in_array( $trigger, array( 'orders', 'spend', 'milestone' ), true ) ? $trigger : 'orders',
'trigger_value' => 'spend' === $trigger ? max( 1, round( (float) ( $in['trigger_value'] ?? 1 ), 2 ) ) : max( 1, (int) round( (float) ( $in['trigger_value'] ?? 1 ) ) ),
'token_expiry_days' => absint( $in['token_expiry_days'] ?? 30 ),
'coupon_expiry_days' => absint( $in['coupon_expiry_days'] ?? 30 ),
'max_spins_per_hour' => max( 1, absint( $in['max_spins_per_hour'] ?? 10 ) ),
'terms' => sanitize_textarea_field( $in['terms'] ?? '' ),
'logo_id' => ( absint( $in['logo_id'] ?? 0 ) && wp_attachment_is_image( absint( $in['logo_id'] ?? 0 ) ) ) ? absint( $in['logo_id'] ) : 0,
'popup' => self::clean_popup( (array) ( $in['popup'] ?? array() ) ),
)
);
break;
case 'appearance':
$style = sanitize_key( $in['style'] ?? 'auto' );
$corners = sanitize_key( $in['corners'] ?? 'rounded' );
$accent = sanitize_hex_color( $in['accent'] ?? '' );
Settings::save_section(
'appearance',
array(
'style' => in_array( $style, array( 'auto', 'custom' ), true ) ? $style : 'auto',
'accent' => $accent ? $accent : '#4f46e5',
'corners' => in_array( $corners, array( 'square', 'rounded', 'pill' ), true ) ? $corners : 'rounded',
)
);
break;
case 'emails':
$out = array();
foreach ( array_keys( $d['emails'] ) as $k ) {
$out[ $k ] = empty( $in[ $k ] ) ? 0 : 1;
if ( ! Settings::has_pro_code() && in_array( $k, array( 'spin_available', 'prize_won', 'draw_winner' ), true ) ) {
$out[ $k ] = (int) Settings::get( 'emails.' . $k ); // Not shown in the free edition: keep as is.
}
}
Settings::save_section( 'emails', $out );
break;
default:
Admin::back( $url, __( 'Unknown settings tab.', 'loyaltyspin' ), true );
}
Audit::log( 'settings_saved', 'tab:' . $tab );
Admin::back( $url, __( 'Settings saved.', 'loyaltyspin' ) );
}
}