PluginProbe
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell / 3.12.7
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell v3.12.7
3.13.3 3.13.2 3.13.1 3.13.0 3.12.13 3.12.12 3.12.11 3.12.10 3.12.9 3.12.8 3.12.7 3.12.6 3.12.5 3.12.4 3.12.3 3.12.1 3.12.2 3.12.0 3.11.1 3.11.0 3.10.9 3.10.8 3.10.7 3.10.6 2.8.16 All 261 releases
wpfunnels / admin / modules / setup-wizard / components / Content / GenerateFunnel.vue

GenerateFunnel.vue in WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell 3.12.7, at admin/modules/setup-wizard/components/Content/GenerateFunnel.vue

528 lines 15.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <template>
2 <div class="wpfnl-mm-setup">
3 <!-- Heading Section -->
4 <div class="wpfnl-mm-setup-header">
5 <h2 class="wpfnl-mm-setup-title">
6 {{ isStoreCheckout ? 'Setting Up Your Store Checkout' : 'Auto-generate Starter Funnel' }}
7 </h2>
8 <p class="wpfnl-mm-setup-subtitle">
9 {{ isStoreCheckout ? 'Creating an optimized checkout experience for your store.' : 'Auto-generating your starter funnel based on your selections.' }}
10 </p>
11 </div>
12
13 <!-- generate-funnel-container -->
14 <div class="wpfnl-generate-funnel-container" :class="isGenerating ? 'generating' : ''">
15 <figure class="wpfnl-generate-funnel-canvas">
16 <img :src="isStoreCheckout ? generateStoreImage : generateFunnelImage" alt="Funnel canvas" width="728" height="250" />
17 </figure>
18
19 <div class="generate-funnel-overlay">
20 <div class="generate-loader" v-if="isGenerating"></div>
21
22 <span
23 class="generated-notice"
24 v-if="statusMessage || errorMessage"
25 :class="{ 'is-error': !!errorMessage }"
26 >
27 <svg v-if="!errorMessage && !isGenerating" xmlns="http://www.w3.org/2000/svg" width="16" height="11" viewBox="0 0 16 11" fill="none">><path d="M14.7511 0.422913C14.287 -0.00764301 13.5333 -0.00737156 13.0686 0.422913L5.49641 7.4387L2.13089 4.32063C1.66617 3.89008 0.91287 3.89008 0.44815 4.32063C-0.0165708 4.75119 -0.0165708 5.44911 0.44815 5.87967L4.65486 9.77711C4.88708 9.99225 5.19157 10.1001 5.49609 10.1001C5.80061 10.1001 6.1054 9.99253 6.33761 9.77711L14.7511 1.98192C15.2158 1.55166 15.2158 0.853441 14.7511 0.422913Z" fill="#239654" stroke="#239654" stroke-width=".2"/></svg>
28 <span>{{ errorMessage || statusMessage }}</span>
29 </span>
30 </div>
31 </div>
32
33 <!-- Buttons -->
34 <div class="wpfnl-mm-choose-goal-buttons">
35 <button class="wpfnl-mm-btn wpfnl-mm-btn-secondary" @click="goBack" :disabled="isGenerating">
36 Back
37 </button>
38
39 <div class="wpfnl-mm-btn-group">
40 <button
41 class="wpfnl-mm-btn wpfnl-mm-btn-primary"
42 @click="handleContinue"
43 :disabled="isGenerating || !createdFunnelId"
44 >
45 Continue
46 <svg width="17" height="12" viewBox="0 0 17 12" fill="none" xmlns="http://www.w3.org/2000/svg">>
47 <path d="M1 6H16M16 6L11 1M16 6L11 11" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
48 </svg>
49 </button>
50 </div>
51 </div>
52 </div>
53 </template>
54
55 <script>
56 import apiFetch from '@wordpress/api-fetch';
57
58 export default {
59 name: 'GenerateFunnel',
60 props: {
61 selectedProduct: {
62 type: Object,
63 default: null
64 },
65 template: {
66 type: Object,
67 default: null
68 },
69 goal: {
70 type: String,
71 default: 'sales'
72 },
73 builder: {
74 type: String,
75 default: 'gutenberg'
76 },
77 mainProduct: {
78 type: Object,
79 default: null
80 },
81 orderBump: {
82 type: Object,
83 default: null
84 },
85 upsellProduct: {
86 type: Object,
87 default: null
88 }
89 },
90 data() {
91 return {
92 generateFunnelImage: window?.setup_wizard_obj?.generate_funnel_image || '',
93 generateStoreImage: window?.setup_wizard_obj?.generate_store_image || '',
94 isGenerating: true,
95 statusMessage: 'Preparing your funnel selections...',
96 errorMessage: '',
97 createdFunnelId: null,
98 firstStepLink: ''
99 };
100 },
101 computed: {
102 isStoreCheckout() {
103 return this.goal === 'improve-checkout';
104 },
105 filteredTemplate() {
106 // Ensure template steps are filtered based on goal
107 if (!this.template || !this.template.steps) return this.template;
108
109 // For sales funnel, ensure only landing, checkout, thankyou
110 if (this.goal === 'sales') {
111 const allowedTypes = ['landing', 'checkout', 'thankyou'];
112 const filteredSteps = this.template.steps.filter(step => {
113 const stepType = step?.step_type || step?.stepType || step?.type;
114 return allowedTypes.includes(stepType);
115 });
116
117 console.log('Filtered template for sales funnel. Original steps:', this.template.steps.length, 'Filtered:', filteredSteps.length);
118
119 return {
120 ...this.template,
121 steps: filteredSteps
122 };
123 }
124
125 return this.template;
126 }
127 },
128 mounted() {
129 this.startFunnelGeneration();
130 },
131 methods: {
132 async startFunnelGeneration() {
133 if (!this.hasValidTemplate()) {
134 this.handleError('Unable to generate a funnel because no template was selected.');
135 return;
136 }
137
138 // Product is only required for non-store-checkout flows
139 if (!this.isStoreCheckout && !this.hasValidProduct()) {
140 this.handleError('Please select a product before generating the funnel.');
141 return;
142 }
143
144 this.isGenerating = true;
145 this.errorMessage = '';
146 this.statusMessage = 'Configuring funnel settings...';
147
148 try {
149 const funnelType = this.getFunnelType();
150 const templateType = this.getTemplateType(funnelType);
151
152 await this.updateGeneralSettings(funnelType);
153
154 this.statusMessage = this.isStoreCheckout ? 'Creating store checkout...' : 'Creating funnel shell...';
155 const funnelResponse = await this.createFunnel(templateType);
156 const funnelId = funnelResponse?.funnelID;
157
158 if (!funnelId) {
159 throw new Error('Funnel could not be created. Please try again.');
160 }
161
162 this.statusMessage = 'Importing template steps...';
163 const { importedSteps, firstStepLink } = await this.importSteps(funnelId);
164
165 // Assign products for non-store-checkout flows
166 if (!this.isStoreCheckout) {
167 this.statusMessage = 'Assigning products...';
168 await this.assignProductsToSteps(importedSteps);
169 }
170
171 this.statusMessage = 'Finalizing funnel...';
172 await this.afterFunnelCreation(funnelId, importedSteps);
173
174 this.createdFunnelId = funnelId;
175 this.firstStepLink = firstStepLink;
176 this.isGenerating = false;
177 this.statusMessage = this.isStoreCheckout ? 'Your store checkout is ready!' : 'The funnel is ready to go.';
178 } catch (error) {
179 console.error('Funnel generation failed:', error);
180 this.handleError(error && error.message ? error.message : 'Unable to generate the funnel. Please try again.');
181 }
182 },
183 hasValidTemplate() {
184 return this.filteredTemplate && Array.isArray(this.filteredTemplate.steps) && this.filteredTemplate.steps.length > 0;
185 },
186 hasValidProduct() {
187 // Check for mainProduct (from BuildFunnel) or selectedProduct (legacy)
188 if (this.mainProduct && this.mainProduct.id) return true;
189 return !!(this.selectedProduct && this.selectedProduct.id);
190 },
191 getFunnelType() {
192 return this.goal === 'leads' ? 'lead' : 'sales';
193 },
194 getTemplateType(funnelType) {
195 return funnelType === 'sales' ? 'wc' : 'lead';
196 },
197 updateGeneralSettings(funnelType) {
198 return new Promise((resolve, reject) => {
199 wpAjaxHelperRequest('update-general-settings', {
200 funnel_type: funnelType,
201 builder: this.builder || 'gutenberg'
202 })
203 .success(() => resolve())
204 .error(error => reject(error));
205 });
206 },
207 createFunnel(templateType) {
208 const steps = this.filterStepsForImport(this.filteredTemplate?.steps || []);
209 const data = {
210 steps: steps,
211 name: this.isStoreCheckout ? 'Store Checkout' : (this.filteredTemplate?.title || 'My First Funnel'),
212 source: 'remote',
213 type: templateType,
214 status: this.isStoreCheckout ? 'publish' : 'draft'
215 };
216
217 // Always pass remoteID - backend needs it to create funnel
218 data.remoteID = this.filteredTemplate?.ID || this.filteredTemplate?.id;
219
220 if (this.isStoreCheckout) {
221 data.is_store_checkout = true;
222 }
223
224 console.log('Creating funnel with data:', {
225 ...data,
226 stepCount: steps.length,
227 stepTypes: steps.map(s => s.step_type || s.stepType || s.type)
228 });
229
230 return new Promise((resolve, reject) => {
231 wpAjaxHelperRequest('wpfunnel-import-funnel', data)
232 .success(response => {
233 console.log('Funnel creation response:', response);
234 resolve(response || {});
235 })
236 .error(error => {
237 console.error('Funnel creation error:', error);
238 reject(error);
239 });
240 });
241 },
242 filterStepsForImport(steps) {
243 const isProActive = window?.setup_wizard_obj?.is_pro_active === 'yes';
244 return (steps || []).filter(step => {
245 const stepType = step?.step_type || step?.stepType || step?.type;
246
247 // For store checkout, only import checkout and thankyou steps
248 if (this.isStoreCheckout) {
249 return stepType === 'checkout' || stepType === 'thankyou';
250 }
251
252 // For sales funnel, only import landing, checkout, and thankyou steps
253 if (this.goal === 'sales') {
254 return stepType === 'landing' || stepType === 'checkout' || stepType === 'thankyou';
255 }
256
257 // For other goals, exclude downsell if pro is not active
258 if (stepType === 'downsell' && !isProActive) {
259 return false;
260 }
261
262 return true;
263 });
264 },
265 async importSteps(funnelId) {
266 const stepsToImport = this.filterStepsForImport(this.filteredTemplate?.steps || []);
267
268 // Additional safety check for sales funnel - ensure no upsell/downsell
269 const finalStepsToImport = this.goal === 'sales'
270 ? stepsToImport.filter(step => {
271 const stepType = step?.step_type || step?.stepType || step?.type;
272 const isAllowed = stepType === 'landing' || stepType === 'checkout' || stepType === 'thankyou';
273 if (!isAllowed) {
274 console.warn(`Skipping step type "${stepType}" for sales funnel`);
275 }
276 return isAllowed;
277 })
278 : stepsToImport;
279
280 console.log('Steps to import for goal "' + this.goal + '":', finalStepsToImport.map(s => s.step_type || s.stepType || s.type));
281
282 const importedSteps = {};
283 let firstStepLink = '';
284
285 for (let index = 0; index < finalStepsToImport.length; index++) {
286 const step = finalStepsToImport[index];
287 const stepType = step?.step_type || step?.stepType || step?.type;
288
289 console.log(`Importing step ${index + 1}/${finalStepsToImport.length}: ${stepType}`);
290
291 try {
292 const response = await apiFetch({
293 path: `${window.setup_wizard_obj.rest_api_url}wpfunnels/v1/steps/wpfunnel-import-step`,
294 method: 'POST',
295 data: {
296 step,
297 funnelID: funnelId,
298 source: 'remote',
299 importType: 'templates'
300 }
301 });
302
303 if (response?.stepID && step?.step_type) {
304 importedSteps[step.step_type] = response.stepID;
305 }
306
307 if (index === 0 && response?.stepViewLink) {
308 firstStepLink = response.stepViewLink;
309 }
310 } catch (error) {
311 console.error(`Error importing step ${step?.step_type || index}:`, error);
312 }
313 }
314
315 if (!Object.keys(importedSteps).length) {
316 throw new Error('Unable to import the selected template steps.');
317 }
318
319 console.log('Successfully imported steps:', Object.keys(importedSteps));
320
321 return { importedSteps, firstStepLink };
322 },
323 async assignProductsToSteps(importedSteps) {
324 const product = this.mainProduct || this.selectedProduct;
325
326 // Assign main product to checkout step
327 if (importedSteps.checkout && product && product.id) {
328 try {
329 await apiFetch({
330 path: `${window.setup_wizard_obj.rest_api_url}wpfunnels/v1/checkout/wpfnl-add-product`,
331 method: 'POST',
332 data: {
333 id: product.id,
334 step_id: importedSteps.checkout,
335 quantity: 1
336 }
337 });
338 } catch (error) {
339 console.error('Error assigning main product:', error);
340 }
341 }
342
343 // Assign order bump to checkout step
344 if (importedSteps.checkout && this.orderBump && this.orderBump.id) {
345 try {
346 let productImage = {
347 id: 0,
348 url: window.setup_wizard_obj.plugin_url ? window.setup_wizard_obj.plugin_url + 'admin/assets/images/placeholder.png' : ''
349 };
350
351 try {
352 const productData = await apiFetch({
353 path: `${window.setup_wizard_obj.rest_api_url}wpfunnels/v1/updateSelectedProduct?product=${this.orderBump.id}`,
354 method: 'GET'
355 });
356
357 if (productData && productData.img) {
358 const imgTag = productData.img;
359 const srcMatch = imgTag.match(/src="([^"]+)"/);
360 if (srcMatch) {
361 productImage.url = srcMatch[1];
362 }
363 const idMatch = imgTag.match(/wp-image-(\d+)/);
364 if (idMatch) {
365 productImage.id = parseInt(idMatch[1]);
366 }
367 }
368 } catch (imgError) {
369 console.error('Could not fetch product image, using placeholder:', imgError);
370 }
371
372 const orderBumpData = [{
373 name: 'Order Bump',
374 selectedStyle: 'style1',
375 isEnabled: true,
376 position: 'after-order',
377 product: this.orderBump.id,
378 productName: this.orderBump.name,
379 chooseVariantName: 'Choose an Option',
380 productSearchName: this.orderBump.name,
381 productType: '',
382 price: '',
383 salePrice: '',
384 quantity: '1',
385 htmlPrice: this.orderBump.priceHtml || '',
386 numericRegularPrice: '',
387 numericSalePrice: '',
388 discountPrice: '',
389 discountPriceHtml: '',
390 productImage: productImage,
391 highLightText: this.orderBump.name,
392 checkBoxLabel: 'Yes, I want this!',
393 productDescriptionText: 'Add this special offer to your order',
394 discountOption: 'original',
395 discountapply: 'regular',
396 discountValue: '',
397 couponName: '',
398 obNextStep: 'default',
399 isReplace: 'no',
400 replaceSettings: {
401 isAllReplace: 'yes',
402 selectedProducts: []
403 },
404 replace: '',
405 obArrowColor: '#EE8134',
406 obPrimaryColor: '#6E42D2',
407 obBgColor: '',
408 obTitleColor: '#363B4E',
409 obHighlightColor: '#6E42D3',
410 obCheckboxTitleColor: '#d9d9d9',
411 obDescriptionColor: '#7A8B9A',
412 isRemoveObBgColor: '',
413 obPriceColor: '#E86F2C',
414 obChooseVariantColor: '#F34D01',
415 prePurchaseUpsell: 'no'
416 }];
417
418 await apiFetch({
419 path: `${window.setup_wizard_obj.rest_api_url}wpfunnels/v1/order-bump`,
420 method: 'POST',
421 data: {
422 value: orderBumpData,
423 stepID: importedSteps.checkout
424 }
425 });
426 } catch (error) {
427 console.error('Error creating order bump:', error);
428 }
429 }
430
431 // Assign upsell product to upsell step
432 if (importedSteps.upsell && this.upsellProduct && this.upsellProduct.id) {
433 try {
434 const upsellData = {
435 id: this.upsellProduct.id,
436 quantity: 1
437 };
438
439 await apiFetch({
440 path: `${window.setup_wizard_obj.rest_api_url}wpfunnels/v1/offer/saveUpsellData/`,
441 method: 'POST',
442 data: {
443 step_id: importedSteps.upsell,
444 product: JSON.stringify(upsellData)
445 }
446 });
447 } catch (error) {
448 console.error('Error assigning upsell product:', error);
449 }
450 }
451 },
452 afterFunnelCreation(funnelId, importedSteps) {
453 const payload = {
454 funnelID: funnelId,
455 source: 'remote',
456 importedSteps: Object.values(importedSteps || {}),
457 goal: this.goal
458 };
459
460 if (this.isStoreCheckout) {
461 payload.is_store_checkout = true;
462 }
463
464 return new Promise(resolve => {
465 wpAjaxHelperRequest('wpfunnel-after-funnel-creation', payload)
466 .success(response => resolve(response))
467 .error(error => {
468 console.error('After funnel creation error:', error);
469 resolve();
470 });
471 });
472 },
473 handleContinue() {
474 if (this.isGenerating || !this.createdFunnelId) {
475 return;
476 }
477
478 this.$emit('next-step', {
479 funnelId: this.createdFunnelId,
480 firstStepLink: this.firstStepLink
481 });
482 },
483 goBack() {
484 if (this.isGenerating) {
485 return;
486 }
487 this.$emit('prev-step');
488 },
489 handleError(message) {
490 this.errorMessage = message;
491 this.isGenerating = false;
492 this.statusMessage = '';
493 this.createdFunnelId = null;
494 this.firstStepLink = '';
495 }
496 }
497 };
498 </script>
499
500 <style scoped>
501 .generate-funnel-overlay {
502 display: flex;
503 flex-direction: column;
504 align-items: center;
505 justify-content: center;
506 gap: 14px;
507 text-align: center;
508 }
509
510 .generate-loader {
511 order: 1;
512 }
513
514 .generated-notice {
515 display: inline-flex;
516 align-items: center;
517 gap: 8px;
518 color: #239654;
519 font-size: 14px;
520 font-weight: 600;
521 order: 2;
522 }
523
524 .generated-notice.is-error {
525 color: #d14343;
526 }
527 </style>
528