PluginProbe
MarqueeX – Smooth Marquee Slider, News Ticker & Post Marquee for Block Editor & Elementor / 0.6.0
MarqueeX – Smooth Marquee Slider, News Ticker & Post Marquee for Block Editor & Elementor v0.6.0
0.6.0 0.5.4 0.5.3 0.5.2 0.5.1 0.5.0 0.4.0 0.3.3 0.3.2 trunk 0.0.1 0.0.2 0.0.3 0.0.4 0.0.5 0.0.6 0.1.0 0.1.1 0.1.2 0.2.0 0.2.1 0.2.2 0.2.3 0.3.0 0.3.1
marqueex / includes / Core / Upsell.php

Upsell.php in MarqueeX – Smooth Marquee Slider, News Ticker & Post Marquee for Block Editor & Elementor 0.6.0, at includes/Core/Upsell.php

315 lines 10.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Wpxero\Marqueex\Core;
4
5 use Wpxero\Marqueex\MarqueeX;
6
7 if (!defined('ABSPATH')) {
8 exit;
9 }
10
11 /**
12 * Upgrade links and locked-feature markup.
13 *
14 * Single source of truth for two things that were previously scattered across
15 * seven files with three different visual styles and two different (one stale)
16 * destination URLs:
17 *
18 * 1. Where an "upgrade" click goes — Freemius' in-admin pricing screen, so the
19 * user never leaves WordPress and the checkout is attributed to the exact
20 * control they clicked.
21 * 2. What a locked Pro feature looks like.
22 *
23 * Free users must always be able to SEE what Pro adds. Hiding a Pro option
24 * entirely (rather than showing it locked) means nobody can want it.
25 */
26 class Upsell {
27
28 /**
29 * Fallback destination when Freemius is unavailable (e.g. its init was
30 * skipped during a plugin upgrade request).
31 */
32 const FALLBACK_URL = 'https://wpxero.com/plugins/marqueex/pricing/';
33
34 /**
35 * Upgrade destination for a given surface.
36 *
37 * @param string $source Short slug identifying the control/screen the click
38 * came from, e.g. 'elementor-animated-heading'. Carried
39 * through as a query arg so CTA performance can be
40 * compared per surface.
41 * @return string
42 */
43 public static function url($source = '') {
44 $url = self::FALLBACK_URL;
45
46 if (function_exists('\\Wpxero\\Marqueex\\marqueex_fs')) {
47 $fs = \Wpxero\Marqueex\marqueex_fs();
48
49 if ($fs && method_exists($fs, 'pricing_url')) {
50 try {
51 if (self::has_license()) {
52 // Freemius removes its pricing screen once a user has a
53 // license, so linking there would 403. Send them to their
54 // account, where the activation flow lives.
55 $url = method_exists($fs, 'get_account_url')
56 ? $fs->get_account_url()
57 : self::FALLBACK_URL;
58 } else {
59 // Offer the trial when one is available and unused — on a
60 // product with no reviews yet, a trial does the reassuring
61 // that social proof normally would.
62 $url = self::is_trial_available()
63 ? $fs->get_trial_url()
64 : $fs->get_upgrade_url();
65 }
66 } catch (\Exception $e) {
67 $url = self::FALLBACK_URL;
68 }
69 }
70 }
71
72 if ($source) {
73 $url = add_query_arg('mqx_source', sanitize_key($source), $url);
74 }
75
76 return $url;
77 }
78
79 /**
80 * Whether a free trial can still be started on this site.
81 *
82 * @return bool
83 */
84 public static function is_trial_available() {
85 static $available = null;
86 if ($available !== null) {
87 return $available;
88 }
89
90 $available = false;
91
92 if (function_exists('\\Wpxero\\Marqueex\\marqueex_fs')) {
93 $fs = \Wpxero\Marqueex\marqueex_fs();
94 if (
95 $fs
96 && method_exists($fs, 'has_trial_plan')
97 && method_exists($fs, 'is_trial_utilized')
98 ) {
99 try {
100 $available = $fs->has_trial_plan()
101 && !$fs->is_trial_utilized()
102 && !self::has_license();
103 } catch (\Exception $e) {
104 $available = false;
105 }
106 }
107 }
108
109 return $available;
110 }
111
112 /**
113 * Label for the call to action.
114 *
115 * Deliberately not "Upgrade Now" — that names the transaction rather than
116 * what the user gets. Callers pass the outcome; this only handles the
117 * trial/no-trial split for the generic case.
118 *
119 * @return string
120 */
121 public static function cta_label() {
122 if (self::has_license()) {
123 return __('Activate your license', 'marqueex');
124 }
125
126 return self::is_trial_available()
127 ? __('Try Pro free', 'marqueex')
128 : __('See what Pro adds', 'marqueex');
129 }
130
131 /**
132 * Reassurance line shown under the call to action.
133 *
134 * @return string
135 */
136 public static function cta_meta() {
137 if (self::has_license()) {
138 return __('You already have a license — activate MarqueeX Pro to switch this on.', 'marqueex');
139 }
140
141 return self::is_trial_available()
142 ? __('Free trial — no card required', 'marqueex')
143 : __('From $59.99 · lifetime · 30-day refund', 'marqueex');
144 }
145
146 /**
147 * Whether upsell markup should be rendered at all.
148 *
149 * @return bool
150 */
151 public static function should_show() {
152 // Shown whenever Pro features are locked — which covers two different
153 // states. Someone with no license needs to be told what Pro adds;
154 // someone who has already paid but not activated needs to be told to
155 // activate, not sold to again. has_license() switches the copy; both
156 // still need a card, or a locked control just fails silently.
157 return !MarqueeX::is_premium_active();
158 }
159
160 /**
161 * Whether Freemius reports that this site already has a paid license.
162 *
163 * Distinct from MarqueeX::is_premium_active(), which additionally requires
164 * the Pro plugin to be loaded. A site can have paid and not yet activated.
165 *
166 * @return bool
167 */
168 public static function has_license() {
169 static $has = null;
170 if ($has !== null) {
171 return $has;
172 }
173
174 $has = false;
175
176 if (function_exists('\\Wpxero\\Marqueex\\marqueex_fs')) {
177 $fs = \Wpxero\Marqueex\marqueex_fs();
178 if ($fs) {
179 try {
180 if (method_exists($fs, 'has_active_valid_license') && $fs->has_active_valid_license()) {
181 $has = true;
182 } elseif (method_exists($fs, 'is_paying') && $fs->is_paying()) {
183 $has = true;
184 }
185 } catch (\Exception $e) {
186 $has = false;
187 }
188 }
189 }
190
191 return $has;
192 }
193
194 /**
195 * Markup for a locked Pro feature.
196 *
197 * One card style used everywhere — Elementor control panels and anywhere
198 * else a locked feature needs describing.
199 *
200 * @param array $args {
201 * @type string $title Feature name.
202 * @type string $description What the user gets, phrased as an outcome.
203 * @type string[] $items Optional list of the specific options unlocked.
204 * @type string $cta Optional CTA label override.
205 * @type string $source Surface slug for attribution.
206 * }
207 * @return string
208 */
209 public static function locked_card(array $args) {
210 $args = wp_parse_args($args, [
211 'title' => '',
212 'description' => '',
213 'items' => [],
214 'cta' => '',
215 'source' => '',
216 ]);
217
218 // A caller's outcome-named CTA ("Unlock these animations") only makes
219 // sense as a sales line. Once a license exists the instruction is
220 // "activate", so the shared label wins.
221 $cta = (!self::has_license() && $args['cta'] !== '') ? $args['cta'] : self::cta_label();
222
223 ob_start();
224 ?>
225 <div class="marqueex-upsell">
226 <div class="marqueex-upsell__head">
227 <i class="eicon-lock" aria-hidden="true"></i>
228 <span class="marqueex-upsell__title"><?php echo esc_html($args['title']); ?></span>
229 <span class="marqueex-upsell__badge"><?php esc_html_e('PRO', 'marqueex'); ?></span>
230 </div>
231
232 <?php if ($args['description']) : ?>
233 <p class="marqueex-upsell__desc"><?php echo esc_html($args['description']); ?></p>
234 <?php endif; ?>
235
236 <?php if (!empty($args['items'])) : ?>
237 <ul class="marqueex-upsell__items">
238 <?php foreach ($args['items'] as $item) : ?>
239 <li><?php echo esc_html($item); ?></li>
240 <?php endforeach; ?>
241 </ul>
242 <?php endif; ?>
243
244 <a class="marqueex-upsell__cta"
245 href="<?php echo esc_url(self::url($args['source'])); ?>"
246 target="_blank"
247 rel="noopener noreferrer"><?php echo esc_html($cta); ?></a>
248
249 <span class="marqueex-upsell__meta"><?php echo esc_html(self::cta_meta()); ?></span>
250 </div>
251 <?php
252 return ob_get_clean();
253 }
254
255 /**
256 * Add a locked-feature card to an Elementor widget as a RAW_HTML control.
257 *
258 * @param \Elementor\Widget_Base|\Elementor\Controls_Stack $element
259 * @param string $control_id
260 * @param array $args See locked_card().
261 * @return void
262 */
263 public static function add_elementor_notice($element, $control_id, array $args) {
264 if (!self::should_show()) {
265 return;
266 }
267
268 $element->add_control(
269 $control_id,
270 [
271 'type' => \Elementor\Controls_Manager::RAW_HTML,
272 'raw' => self::locked_card($args),
273 'content_classes' => 'marqueex-upsell-wrap',
274 ]
275 );
276 }
277
278 /**
279 * Merge Pro options into a select control's option list, marking the locked
280 * ones so free users can see they exist.
281 *
282 * Elementor has no per-option disabled state, so a locked option is labelled
283 * with a padlock and reverts on select (handled in elementor-editor.js). The
284 * render side already falls back to the free default, so a stored Pro value
285 * on an unlicensed site is harmless.
286 *
287 * @param array $free_options Options available to everyone.
288 * @param array $pro_options Options that require a license.
289 * @return array
290 */
291 public static function merge_select_options(array $free_options, array $pro_options) {
292 if (!self::should_show()) {
293 return array_merge($free_options, $pro_options);
294 }
295
296 $locked = [];
297 foreach ($pro_options as $key => $label) {
298 /* translators: %s: name of a Pro-only option in a dropdown. */
299 $locked[$key] = sprintf(__('%s (Pro)', 'marqueex'), $label);
300 }
301
302 return array_merge($free_options, $locked);
303 }
304
305 /**
306 * Option keys that require a license, for the editor script to mark up.
307 *
308 * @param array $pro_options
309 * @return string[]
310 */
311 public static function locked_option_keys(array $pro_options) {
312 return self::should_show() ? array_keys($pro_options) : [];
313 }
314 }
315