array(
'coins' => 1,
'tiers' => 1,
'gifts' => 1,
'spin' => 1,
'draw' => 1,
),
'general' => array(
'qualifying_statuses' => array( 'processing', 'completed' ),
'show_odds' => 1,
'min_age_notice' => 1,
'excluded_states' => '',
'delete_data' => 0,
),
'coins' => array(
'singular' => 'Coin',
'plural' => 'Coins',
'icon' => '🪙',
'icon_id' => 0,
'earn_coins' => 10,
'earn_per_amount' => 100,
'redeem_coins' => 100,
'redeem_value' => 10,
'min_redeem' => 100,
'max_redeem_percent' => 50,
'expiry_months' => 12,
'reminder_days' => 15,
'bonus_signup' => 50,
'bonus_first_order' => 100,
'bonus_review' => 20,
'bonus_birthday' => 100,
'daily_earn_cap' => 0,
'show_on_product' => 1,
'show_on_loop' => 1,
),
'tiers' => array(
'levels' => array(
array(
'name' => 'Bronze',
'threshold' => 0,
'percent' => 0,
),
array(
'name' => 'Silver',
'threshold' => 10000,
'percent' => 3,
),
array(
'name' => 'Gold',
'threshold' => 50000,
'percent' => 5,
),
),
'cap_amount' => 1000,
'exclude_sale' => 1,
'exclude_cats' => array(),
'stack_coupons' => 1,
),
'gifts' => array(
'rules' => array(),
),
'spin' => array(
'trigger' => 'orders',
'trigger_value' => 3,
'token_expiry_days' => 30,
'coupon_expiry_days' => 30,
'max_spins_per_hour' => 10,
'logo_id' => 0,
'popup' => array(
'enabled' => 1,
'triggers' => array( 'login', 'thankyou' ),
'delay' => 8,
'scroll' => 50,
'audience' => 'has_spins',
'frequency' => 'session',
'devices' => 'all',
),
'terms' => 'One spin per token. Prizes are decided by the store server at random according to the odds shown. Coupons are single-use, tied to your account and expire as stated. Not redeemable for cash.',
),
'appearance' => array(
'style' => 'auto',
'accent' => '#4f46e5',
'corners' => 'rounded',
),
'emails' => array(
'coins_earned' => 1,
'expiry_warning' => 1,
'spin_available' => 1,
'prize_won' => 1,
'draw_winner' => 1,
),
);
}
/**
* All settings merged over defaults.
*
* @return array
*/
public static function all() {
if ( null === self::$cache ) {
$saved = get_option( self::OPTION, array() );
self::$cache = self::merge( self::defaults(), is_array( $saved ) ? $saved : array() );
}
return self::$cache;
}
/**
* Read one value by dotted path.
*
* @param string $path e.g. "coins.plural".
* @param mixed $fallback Fallback value.
* @return mixed
*/
public static function get( $path, $fallback = null ) {
$value = self::all();
foreach ( explode( '.', $path ) as $key ) {
if ( ! is_array( $value ) || ! array_key_exists( $key, $value ) ) {
return $fallback;
}
$value = $value[ $key ];
}
return $value;
}
/**
* Save a full section.
*
* @param string $section Section key.
* @param array $values Sanitized values.
*/
public static function save_section( $section, array $values ) {
$all = self::all();
$all[ $section ] = $values;
update_option( self::OPTION, $all, false );
self::$cache = null;
}
/**
* Is a module switched on?
*
* @param string $module Module key.
* @return bool
*/
public static function module_on( $module ) {
if ( in_array( $module, self::PRO_MODULES, true ) && ! self::is_pro() ) {
return false;
}
return (bool) self::get( 'modules.' . $module, false );
}
/**
* Modules that ship only in the Pro edition.
*/
const PRO_MODULES = array( 'gifts', 'spin', 'draw' );
/**
* Is the Pro module code in this build? (The free download has those folders removed.)
*
* @return bool
*/
public static function has_pro_code() {
return is_dir( LOYALTYPRESS_DIR . 'includes/draw' ) && is_dir( LOYALTYPRESS_DIR . 'includes/spin' );
}
/**
* Pro is on: Pro code present and a paid licence (or licensing not configured yet, during development).
*
* @return bool
*/
public static function is_pro() {
static $pro = null;
if ( null === $pro ) {
$pro = self::has_pro_code() && Licensing\Licensing::is_paying();
}
return $pro;
}
/**
* Order statuses (without wc- prefix) that count as "processed".
*
* @return string[]
*/
public static function qualifying_statuses() {
$statuses = (array) self::get( 'general.qualifying_statuses', array( 'processing', 'completed' ) );
return (array) apply_filters( 'loyaltypress_qualifying_statuses', $statuses );
}
/**
* Chance-based features (spin, draw) can be switched off for some states/regions
* where local law restricts them. Entries: state code ("TN"), country:state ("IN:TN") or a whole country ("US:*").
*
* @param string $country Billing country code.
* @param string $state Billing state code.
* @return bool
*/
public static function region_allowed( $country, $state ) {
$list = array_filter( array_map( 'trim', explode( ',', strtoupper( (string) self::get( 'general.excluded_states', '' ) ) ) ) );
if ( ! $list ) {
return true;
}
$state = strtoupper( (string) $state );
$country = strtoupper( (string) $country );
return ! in_array( $state, $list, true ) && ! in_array( $country . ':' . $state, $list, true ) && ! in_array( $country . ':*', $list, true );
}
/**
* Region check for a user (billing address).
*
* @param int $user_id User.
* @return bool
*/
public static function user_region_allowed( $user_id ) {
return self::region_allowed( get_user_meta( $user_id, 'billing_country', true ), get_user_meta( $user_id, 'billing_state', true ) );
}
/**
* Coin icon as HTML: the uploaded image if there is one, otherwise the emoji/symbol.
*
* @return string
*/
public static function coin_icon_html() {
$id = (int) self::get( 'coins.icon_id', 0 );
if ( $id ) {
$url = wp_get_attachment_image_url( $id, 'thumbnail' );
if ( $url ) {
return '
';
}
}
$icon = (string) self::get( 'coins.icon', '' );
return '' === $icon ? '' : '' . esc_html( $icon ) . '';
}
/**
* Coin icon for plain-text places (order notes, plain emails). Empty when an image is used.
*
* @return string
*/
public static function coin_icon_text() {
return (int) self::get( 'coins.icon_id', 0 ) ? '' : (string) self::get( 'coins.icon', '' );
}
/**
* Coin label helper.
*
* @param int $amount Amount (for singular/plural).
* @return string
*/
public static function coin_label( $amount = 2 ) {
return 1 === abs( (int) $amount ) ? (string) self::get( 'coins.singular' ) : (string) self::get( 'coins.plural' );
}
/**
* Recursive merge where saved values win but lists are replaced, not merged.
*
* @param array $defaults Defaults.
* @param array $saved Saved values.
* @return array
*/
private static function merge( array $defaults, array $saved ) {
foreach ( $saved as $key => $value ) {
if ( is_array( $value ) && isset( $defaults[ $key ] ) && is_array( $defaults[ $key ] ) && ! wp_is_numeric_array( $defaults[ $key ] ) && 'levels' !== $key && 'rules' !== $key ) {
$defaults[ $key ] = self::merge( $defaults[ $key ], $value );
} else {
$defaults[ $key ] = $value;
}
}
return $defaults;
}
}