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 ? '' : ''; } /** * 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; } }