| 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 |
|