| 1 |
<template> |
| 2 |
<div class="wpfnl-store-checkout-step"> |
| 3 |
|
| 4 |
<!-- Phase: Select Template --> |
| 5 |
<div v-if="phase === 'select'" class="wpfnl-mm-choose-template"> |
| 6 |
<div class="wpfnl-mm-setup-header"> |
| 7 |
<h2 class="wpfnl-mm-setup-title">Choose Your Store Checkout Template</h2> |
| 8 |
<p class="wpfnl-mm-setup-subtitle"> |
| 9 |
Pick a ready-made checkout designed to reduce cart abandonment and boost conversions. |
| 10 |
</p> |
| 11 |
</div> |
| 12 |
|
| 13 |
<div class="wpfnl-mm-templates-grid" v-if="!loading"> |
| 14 |
<div |
| 15 |
v-for="template in templates" |
| 16 |
:key="template.ID" |
| 17 |
class="wpfnl-mm-template-card" |
| 18 |
:class="{ 'wpfnl-mm-template-card--suggested': isSuggestedTemplate(template) }" |
| 19 |
@click="showPreview(template)" |
| 20 |
> |
| 21 |
<div class="wpfnl-mm-template-card-preview"> |
| 22 |
<img :src="getCardImage(template)" :alt="template.title" v-if="getCardImage(template)"> |
| 23 |
<div class="wpfnl-mm-template-card-overlay"> |
| 24 |
<button class="wpfnl-mm-btn-preview" @click.stop="showPreview(template)">Preview</button> |
| 25 |
</div> |
| 26 |
<!-- Suggested badge --> |
| 27 |
<div class="wpfnl-mm-template-card-suggested" v-if="isSuggestedTemplate(template)"> |
| 28 |
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">> |
| 29 |
<path d="M5 1L6.18 3.4L8.8 3.78L6.9 5.64L7.36 8.25L5 6.99L2.64 8.25L3.1 5.64L1.2 3.78L3.82 3.4L5 1Z" fill="white" stroke="white" stroke-width="0.5" stroke-linejoin="round"/> |
| 30 |
</svg> |
| 31 |
Suggested |
| 32 |
</div> |
| 33 |
</div> |
| 34 |
<div class="wpfnl-mm-template-card-divider"></div> |
| 35 |
<div class="wpfnl-mm-template-card-body"> |
| 36 |
<h3 class="wpfnl-mm-template-card-title">{{ template.title }}</h3> |
| 37 |
<span class="wpfnl-mm-template-card-steps" v-if="template.steps">{{ template.steps.length }} Steps</span> |
| 38 |
</div> |
| 39 |
</div> |
| 40 |
</div> |
| 41 |
|
| 42 |
<div v-else class="wpfnl-mm-templates-grid wpfnl-mm-templates-skeleton"> |
| 43 |
<div v-for="n in 6" :key="n" class="wpfnl-mm-template-card wpfnl-mm-template-card--skeleton"> |
| 44 |
<div class="wpfnl-mm-template-card-preview wpfnl-mm-skeleton-block"></div> |
| 45 |
<div class="wpfnl-mm-template-card-divider"></div> |
| 46 |
<div class="wpfnl-mm-template-card-body"> |
| 47 |
<div class="wpfnl-mm-skeleton-block wpfnl-mm-skeleton-title"></div> |
| 48 |
<div class="wpfnl-mm-skeleton-block wpfnl-mm-skeleton-steps"></div> |
| 49 |
</div> |
| 50 |
</div> |
| 51 |
</div> |
| 52 |
|
| 53 |
<div class="wpfnl-mm-choose-goal-buttons wpfnl-mm-buttons-container wpfnl-sc-footer-actions"> |
| 54 |
<button class="wpfnl-mm-btn wpfnl-mm-btn-secondary" @click="goBack"> |
| 55 |
<svg width="17" height="12" viewBox="0 0 17 12" fill="none" xmlns="http://www.w3.org/2000/svg">> |
| 56 |
<path d="M16 6H1M1 6L6 1M1 6L6 11" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> |
| 57 |
</svg> |
| 58 |
Back |
| 59 |
</button> |
| 60 |
<button class="wpfnl-mm-btn wpfnl-mm-btn-skip" @click="skipCheckout"> |
| 61 |
Skip |
| 62 |
</button> |
| 63 |
</div> |
| 64 |
|
| 65 |
<PreviewTemplate |
| 66 |
v-if="showPreviewModal" |
| 67 |
:template="previewTemplate" |
| 68 |
:isStoreCheckout="true" |
| 69 |
:isSalesFunnel="false" |
| 70 |
@close="closePreview" |
| 71 |
@import="importFromPreview" |
| 72 |
/> |
| 73 |
</div> |
| 74 |
|
| 75 |
<!-- Phase: Generating --> |
| 76 |
<div v-else-if="phase === 'generating'" class="wpfnl-mm-setup"> |
| 77 |
<div class="wpfnl-mm-setup-header"> |
| 78 |
<h2 class="wpfnl-mm-setup-title">Setting Up Your Store Checkout</h2> |
| 79 |
<p class="wpfnl-mm-setup-subtitle">Creating an optimized checkout experience for your store.</p> |
| 80 |
</div> |
| 81 |
|
| 82 |
<div class="wpfnl-generate-funnel-container generating"> |
| 83 |
<figure class="wpfnl-generate-funnel-canvas"> |
| 84 |
<img :src="generateStoreImage" alt="Store checkout setup" width="728" height="250" /> |
| 85 |
</figure> |
| 86 |
<div class="generate-funnel-overlay"> |
| 87 |
<div class="generate-loader"></div> |
| 88 |
<span class="generated-notice" v-if="statusMessage"> |
| 89 |
<span>{{ statusMessage }}</span> |
| 90 |
</span> |
| 91 |
<span class="generated-notice is-error" v-if="errorMessage"> |
| 92 |
{{ errorMessage }} |
| 93 |
</span> |
| 94 |
</div> |
| 95 |
</div> |
| 96 |
</div> |
| 97 |
|
| 98 |
<!-- Phase: Complete (Celebration) --> |
| 99 |
<div v-else-if="phase === 'complete'" class="wpfnl-store-celebration"> |
| 100 |
<canvas ref="confettiCanvas" class="wpfnl-confetti-canvas"></canvas> |
| 101 |
|
| 102 |
<div class="wpfnl-celebration-content"> |
| 103 |
<div class="wpfnl-celebration-icon"> |
| 104 |
<svg width="72" height="72" viewBox="0 0 72 72" fill="none" xmlns="http://www.w3.org/2000/svg">> |
| 105 |
<circle cx="36" cy="36" r="36" fill="#EDE5FF"/> |
| 106 |
<circle cx="36" cy="36" r="26" fill="#6E42D3"/> |
| 107 |
<path d="M26 36L33 43L46 29" stroke="white" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/> |
| 108 |
</svg> |
| 109 |
</div> |
| 110 |
|
| 111 |
<h2 class="wpfnl-celebration-title">Setup Complete! 🎉</h2> |
| 112 |
<p class="wpfnl-celebration-subtitle"> |
| 113 |
<template v-if="skipped"> |
| 114 |
Your setup is complete. You can configure Store Checkout anytime from the WPFunnels settings. |
| 115 |
</template> |
| 116 |
<template v-else> |
| 117 |
Your store checkout is ready. Start converting visitors into customers. |
| 118 |
</template> |
| 119 |
</p> |
| 120 |
|
| 121 |
<div class="wpfnl-celebration-actions"> |
| 122 |
<button class="wpfnl-mm-btn wpfnl-mm-btn-secondary" @click="goToDashboard"> |
| 123 |
Go to Dashboard |
| 124 |
</button> |
| 125 |
<button v-if="!skipped" class="wpfnl-mm-btn wpfnl-mm-btn-primary" @click="goToEditor"> |
| 126 |
Go to Editor |
| 127 |
<svg width="17" height="12" viewBox="0 0 17 12" fill="none" xmlns="http://www.w3.org/2000/svg">> |
| 128 |
<path d="M1 6H16M16 6L11 1M16 6L11 11" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> |
| 129 |
</svg> |
| 130 |
</button> |
| 131 |
</div> |
| 132 |
</div> |
| 133 |
</div> |
| 134 |
|
| 135 |
</div> |
| 136 |
</template> |
| 137 |
|
| 138 |
<script> |
| 139 |
import apiFetch from '@wordpress/api-fetch'; |
| 140 |
import { addQueryArgs } from '@wordpress/url'; |
| 141 |
import PreviewTemplate from './PreviewTemplate.vue'; |
| 142 |
|
| 143 |
export default { |
| 144 |
name: 'StoreCheckout', |
| 145 |
components: { PreviewTemplate }, |
| 146 |
props: { |
| 147 |
builder: { |
| 148 |
type: String, |
| 149 |
default: 'gutenberg' |
| 150 |
}, |
| 151 |
prefetchedTemplates: { |
| 152 |
type: Array, |
| 153 |
default: () => [] |
| 154 |
}, |
| 155 |
agreeToShare: { |
| 156 |
type: Boolean, |
| 157 |
default: false |
| 158 |
} |
| 159 |
}, |
| 160 |
data() { |
| 161 |
return { |
| 162 |
phase: 'select', |
| 163 |
skipped: false, |
| 164 |
loading: true, |
| 165 |
templates: [], |
| 166 |
selectedTemplateId: null, |
| 167 |
selectedTemplate: null, |
| 168 |
showPreviewModal: false, |
| 169 |
previewTemplate: null, |
| 170 |
statusMessage: '', |
| 171 |
errorMessage: '', |
| 172 |
createdFunnelId: null, |
| 173 |
generateStoreImage: window?.setup_wizard_obj?.generate_store_image || '', |
| 174 |
}; |
| 175 |
}, |
| 176 |
mounted() { |
| 177 |
const cached = this.prefetchedTemplates; |
| 178 |
if (cached && cached.length > 0) { |
| 179 |
this.templates = this.filterTemplates(cached); |
| 180 |
this.loading = false; |
| 181 |
} else { |
| 182 |
this.fetchTemplates(); |
| 183 |
} |
| 184 |
}, |
| 185 |
watch: { |
| 186 |
prefetchedTemplates(val) { |
| 187 |
if (!this.loading || !val || !val.length) return; |
| 188 |
this.templates = this.filterTemplates(val); |
| 189 |
this.loading = false; |
| 190 |
}, |
| 191 |
}, |
| 192 |
methods: { |
| 193 |
fetchTemplates() { |
| 194 |
this.loading = true; |
| 195 |
let builderParam = this.builder === 'divi' ? 'divi-builder' : (this.builder || 'gutenberg'); |
| 196 |
|
| 197 |
const path = addQueryArgs( |
| 198 |
`${window.setup_wizard_obj.rest_api_url}wpfunnels/v1/templates/get_templates`, |
| 199 |
{ builder: builderParam, type: 'store_checkout' } |
| 200 |
); |
| 201 |
|
| 202 |
apiFetch({ path }) |
| 203 |
.then(response => { |
| 204 |
if (response.success && response.templates && response.templates.length > 0) { |
| 205 |
this.templates = this.filterTemplates(response.templates); |
| 206 |
} else if (builderParam !== 'gutenberg') { |
| 207 |
this.fetchFallback(); |
| 208 |
return; |
| 209 |
} |
| 210 |
}) |
| 211 |
.catch(() => { |
| 212 |
if (this.builder !== 'gutenberg') this.fetchFallback(); |
| 213 |
}) |
| 214 |
.finally(() => { this.loading = false; }); |
| 215 |
}, |
| 216 |
|
| 217 |
fetchFallback() { |
| 218 |
const path = addQueryArgs( |
| 219 |
`${window.setup_wizard_obj.rest_api_url}wpfunnels/v1/templates/get_templates`, |
| 220 |
{ builder: 'gutenberg', type: 'store_checkout' } |
| 221 |
); |
| 222 |
apiFetch({ path }) |
| 223 |
.then(response => { |
| 224 |
if (response.success && response.templates) { |
| 225 |
this.templates = this.filterTemplates(response.templates); |
| 226 |
} |
| 227 |
}) |
| 228 |
.catch(() => {}) |
| 229 |
.finally(() => { this.loading = false; }); |
| 230 |
}, |
| 231 |
|
| 232 |
isSuggestedTemplate(template) { |
| 233 |
return (template.title || '').toLowerCase().includes('instant checkout'); |
| 234 |
}, |
| 235 |
|
| 236 |
filterTemplates(templates) { |
| 237 |
const filtered = templates |
| 238 |
.filter(t => t.templateType !== 'pro') |
| 239 |
.filter(t => { |
| 240 |
if (!t.steps || !Array.isArray(t.steps)) return false; |
| 241 |
const types = t.steps.map(s => s.step_type); |
| 242 |
return types.includes('checkout') && types.includes('thankyou'); |
| 243 |
}) |
| 244 |
.map(t => { |
| 245 |
const checkout = t.steps.find(s => s.step_type === 'checkout'); |
| 246 |
const thankyou = t.steps.find(s => s.step_type === 'thankyou'); |
| 247 |
return { ...t, steps: [checkout, thankyou].filter(Boolean) }; |
| 248 |
}) |
| 249 |
.sort((a, b) => a.ID - b.ID); |
| 250 |
|
| 251 |
// Pin the suggested template first |
| 252 |
const suggestedIdx = filtered.findIndex(t => this.isSuggestedTemplate(t)); |
| 253 |
if (suggestedIdx > 0) { |
| 254 |
const [suggested] = filtered.splice(suggestedIdx, 1); |
| 255 |
filtered.unshift(suggested); |
| 256 |
} |
| 257 |
|
| 258 |
return filtered; |
| 259 |
}, |
| 260 |
|
| 261 |
getCardImage(template) { |
| 262 |
if (template.steps) { |
| 263 |
const checkoutStep = template.steps.find(s => s.step_type === 'checkout'); |
| 264 |
if (checkoutStep && checkoutStep.featured_image) return checkoutStep.featured_image; |
| 265 |
} |
| 266 |
return template.featured_image; |
| 267 |
}, |
| 268 |
|
| 269 |
selectTemplate(template) { |
| 270 |
this.selectedTemplateId = template.ID; |
| 271 |
this.selectedTemplate = template; |
| 272 |
}, |
| 273 |
|
| 274 |
showPreview(template) { |
| 275 |
this.previewTemplate = template; |
| 276 |
this.showPreviewModal = true; |
| 277 |
}, |
| 278 |
|
| 279 |
closePreview() { |
| 280 |
this.showPreviewModal = false; |
| 281 |
this.previewTemplate = null; |
| 282 |
}, |
| 283 |
|
| 284 |
importFromPreview(template) { |
| 285 |
this.closePreview(); |
| 286 |
this.selectedTemplateId = template.ID || template.id; |
| 287 |
this.selectedTemplate = template; |
| 288 |
this.startImport(); |
| 289 |
}, |
| 290 |
|
| 291 |
startImport() { |
| 292 |
if (!this.selectedTemplateId) return; |
| 293 |
this.phase = 'generating'; |
| 294 |
this.$emit('store-checkout-phase', 'generating'); |
| 295 |
this.generateFunnel(); |
| 296 |
}, |
| 297 |
|
| 298 |
async generateFunnel() { |
| 299 |
this.statusMessage = 'Configuring settings...'; |
| 300 |
this.errorMessage = ''; |
| 301 |
|
| 302 |
try { |
| 303 |
await this.updateGeneralSettings('sales'); |
| 304 |
|
| 305 |
this.statusMessage = 'Creating store checkout...'; |
| 306 |
const funnelResponse = await this.createFunnel(); |
| 307 |
const funnelId = funnelResponse?.funnelID; |
| 308 |
|
| 309 |
if (!funnelId) throw new Error('Funnel could not be created. Please try again.'); |
| 310 |
|
| 311 |
this.statusMessage = 'Importing template steps...'; |
| 312 |
const { importedSteps } = await this.importSteps(funnelId); |
| 313 |
|
| 314 |
this.statusMessage = 'Finalizing...'; |
| 315 |
await this.afterFunnelCreation(funnelId, importedSteps); |
| 316 |
|
| 317 |
this.createdFunnelId = funnelId; |
| 318 |
this.maybeCreateContact(); |
| 319 |
this.handleCompleteTracking(); |
| 320 |
this.phase = 'complete'; |
| 321 |
this.$emit('store-checkout-phase', 'complete'); |
| 322 |
this.$nextTick(() => this.launchConfetti()); |
| 323 |
} catch (error) { |
| 324 |
this.errorMessage = error?.message || 'Unable to generate the funnel. Please try again.'; |
| 325 |
this.statusMessage = ''; |
| 326 |
} |
| 327 |
}, |
| 328 |
|
| 329 |
updateGeneralSettings(funnelType) { |
| 330 |
return new Promise((resolve, reject) => { |
| 331 |
wpAjaxHelperRequest('update-general-settings', { |
| 332 |
funnel_type: funnelType, |
| 333 |
builder: this.builder || 'gutenberg' |
| 334 |
}) |
| 335 |
.success(() => resolve()) |
| 336 |
.error(err => reject(err)); |
| 337 |
}); |
| 338 |
}, |
| 339 |
|
| 340 |
createFunnel() { |
| 341 |
const steps = this.filterStepsForImport(this.selectedTemplate?.steps || []); |
| 342 |
const data = { |
| 343 |
steps, |
| 344 |
name: 'Store Checkout', |
| 345 |
source: 'remote', |
| 346 |
type: 'wc', |
| 347 |
status: 'publish', |
| 348 |
remoteID: this.selectedTemplate?.ID || this.selectedTemplate?.id, |
| 349 |
is_store_checkout: true, |
| 350 |
}; |
| 351 |
|
| 352 |
return new Promise((resolve, reject) => { |
| 353 |
wpAjaxHelperRequest('wpfunnel-import-funnel', data) |
| 354 |
.success(response => resolve(response || {})) |
| 355 |
.error(err => reject(err)); |
| 356 |
}); |
| 357 |
}, |
| 358 |
|
| 359 |
filterStepsForImport(steps) { |
| 360 |
return (steps || []).filter(step => { |
| 361 |
const type = step?.step_type || step?.stepType || step?.type; |
| 362 |
return type === 'checkout' || type === 'thankyou'; |
| 363 |
}); |
| 364 |
}, |
| 365 |
|
| 366 |
async importSteps(funnelId) { |
| 367 |
const stepsToImport = this.filterStepsForImport(this.selectedTemplate?.steps || []); |
| 368 |
const importedSteps = {}; |
| 369 |
|
| 370 |
for (const step of stepsToImport) { |
| 371 |
try { |
| 372 |
const response = await apiFetch({ |
| 373 |
path: `${window.setup_wizard_obj.rest_api_url}wpfunnels/v1/steps/wpfunnel-import-step`, |
| 374 |
method: 'POST', |
| 375 |
data: { step, funnelID: funnelId, source: 'remote', importType: 'templates' } |
| 376 |
}); |
| 377 |
|
| 378 |
if (response?.stepID && step?.step_type) { |
| 379 |
importedSteps[step.step_type] = response.stepID; |
| 380 |
} |
| 381 |
} catch (err) { |
| 382 |
console.error(`Error importing step ${step?.step_type}:`, err); |
| 383 |
} |
| 384 |
} |
| 385 |
|
| 386 |
if (!Object.keys(importedSteps).length) { |
| 387 |
throw new Error('Unable to import the selected template steps.'); |
| 388 |
} |
| 389 |
|
| 390 |
return { importedSteps }; |
| 391 |
}, |
| 392 |
|
| 393 |
afterFunnelCreation(funnelId, importedSteps) { |
| 394 |
return new Promise(resolve => { |
| 395 |
wpAjaxHelperRequest('wpfunnel-after-funnel-creation', { |
| 396 |
funnelID: funnelId, |
| 397 |
source: 'remote', |
| 398 |
importedSteps: Object.values(importedSteps || {}), |
| 399 |
goal: 'improve-checkout', |
| 400 |
is_store_checkout: true, |
| 401 |
}) |
| 402 |
.success(response => resolve(response)) |
| 403 |
.error(() => resolve()); |
| 404 |
}); |
| 405 |
}, |
| 406 |
|
| 407 |
handleCompleteTracking() { |
| 408 |
const wizardObj = window.setup_wizard_obj || {}; |
| 409 |
const restApiUrl = wizardObj.rest_api_url || ''; |
| 410 |
if (!restApiUrl) return; |
| 411 |
|
| 412 |
const normalizedUrl = restApiUrl.endsWith('/') ? restApiUrl : `${restApiUrl}/`; |
| 413 |
apiFetch({ |
| 414 |
url: `${normalizedUrl}wpfunnels/v1/setup-wizard/complete-step`, |
| 415 |
method: 'POST', |
| 416 |
data: { funnelId: this.createdFunnelId, action: 'completed', goal: 'improve-checkout', total_steps: 3 }, |
| 417 |
}).catch(() => {}); |
| 418 |
}, |
| 419 |
|
| 420 |
maybeCreateContact() { |
| 421 |
if (!this.agreeToShare) return; |
| 422 |
const wizardObj = window.setup_wizard_obj || {}; |
| 423 |
if (!wizardObj.rest_api_url) return; |
| 424 |
|
| 425 |
apiFetch({ |
| 426 |
path: `${wizardObj.rest_api_url}wpfunnels/v1/settings/create-contact/`, |
| 427 |
method: 'POST', |
| 428 |
data: { email: wizardObj.admin_email, name: wizardObj.admin_name }, |
| 429 |
}).catch(() => {}); |
| 430 |
}, |
| 431 |
|
| 432 |
async goToEditor() { |
| 433 |
const wizardObj = window.setup_wizard_obj || {}; |
| 434 |
const adminUrl = wizardObj.admin_url || ''; |
| 435 |
window.location.href = `${adminUrl}admin.php?page=edit_funnel&id=${this.createdFunnelId}`; |
| 436 |
}, |
| 437 |
|
| 438 |
async goToDashboard() { |
| 439 |
const wizardObj = window.setup_wizard_obj || {}; |
| 440 |
window.location.href = wizardObj.dashboard_url || `${wizardObj.admin_url}admin.php?page=wpfunnels`; |
| 441 |
}, |
| 442 |
|
| 443 |
skipCheckout() { |
| 444 |
this.skipped = true; |
| 445 |
this.phase = 'complete'; |
| 446 |
this.$emit('store-checkout-phase', 'skipped'); |
| 447 |
this.$nextTick(() => this.launchConfetti()); |
| 448 |
this.maybeCreateContact(); |
| 449 |
this.handleSkipTracking(); |
| 450 |
}, |
| 451 |
|
| 452 |
handleSkipTracking() { |
| 453 |
const wizardObj = window.setup_wizard_obj || {}; |
| 454 |
const restApiUrl = wizardObj.rest_api_url || ''; |
| 455 |
if (!restApiUrl) return; |
| 456 |
|
| 457 |
const normalizedUrl = restApiUrl.endsWith('/') ? restApiUrl : `${restApiUrl}/`; |
| 458 |
apiFetch({ |
| 459 |
url: `${normalizedUrl}wpfunnels/v1/setup-wizard/complete-step`, |
| 460 |
method: 'POST', |
| 461 |
data: { action: 'skipped', goal: 'improve-checkout', total_steps: 3 }, |
| 462 |
}).catch(() => {}); |
| 463 |
}, |
| 464 |
|
| 465 |
goBack() { |
| 466 |
this.$emit('prev-step'); |
| 467 |
}, |
| 468 |
|
| 469 |
launchConfetti() { |
| 470 |
const canvas = this.$refs.confettiCanvas; |
| 471 |
if (!canvas) return; |
| 472 |
|
| 473 |
const ctx = canvas.getContext('2d'); |
| 474 |
canvas.width = window.innerWidth; |
| 475 |
canvas.height = window.innerHeight; |
| 476 |
|
| 477 |
const colors = ['#6E42D3', '#F59E0B', '#10B981', '#EF4444', '#3B82F6', '#EC4899', '#8B5CF6', '#F97316', '#06B6D4']; |
| 478 |
const pieces = Array.from({ length: 220 }, () => ({ |
| 479 |
x: Math.random() * canvas.width, |
| 480 |
y: Math.random() * -canvas.height * 0.5, |
| 481 |
w: Math.random() * 12 + 5, |
| 482 |
h: Math.random() * 6 + 3, |
| 483 |
color: colors[Math.floor(Math.random() * colors.length)], |
| 484 |
vy: Math.random() * 3.5 + 1.5, |
| 485 |
vx: (Math.random() - 0.5) * 2.5, |
| 486 |
angle: Math.random() * Math.PI * 2, |
| 487 |
va: (Math.random() - 0.5) * 0.18, |
| 488 |
opacity: 1, |
| 489 |
})); |
| 490 |
|
| 491 |
const startTime = Date.now(); |
| 492 |
const duration = 5000; |
| 493 |
|
| 494 |
const draw = () => { |
| 495 |
const elapsed = Date.now() - startTime; |
| 496 |
if (elapsed > duration) { |
| 497 |
ctx.clearRect(0, 0, canvas.width, canvas.height); |
| 498 |
return; |
| 499 |
} |
| 500 |
|
| 501 |
ctx.clearRect(0, 0, canvas.width, canvas.height); |
| 502 |
|
| 503 |
pieces.forEach(p => { |
| 504 |
p.y += p.vy; |
| 505 |
p.x += p.vx; |
| 506 |
p.angle += p.va; |
| 507 |
p.opacity = elapsed > 3500 ? Math.max(0, 1 - (elapsed - 3500) / 1500) : 1; |
| 508 |
|
| 509 |
ctx.save(); |
| 510 |
ctx.globalAlpha = p.opacity; |
| 511 |
ctx.translate(p.x + p.w / 2, p.y + p.h / 2); |
| 512 |
ctx.rotate(p.angle); |
| 513 |
ctx.fillStyle = p.color; |
| 514 |
ctx.fillRect(-p.w / 2, -p.h / 2, p.w, p.h); |
| 515 |
ctx.restore(); |
| 516 |
}); |
| 517 |
|
| 518 |
requestAnimationFrame(draw); |
| 519 |
}; |
| 520 |
|
| 521 |
draw(); |
| 522 |
}, |
| 523 |
} |
| 524 |
}; |
| 525 |
</script> |
| 526 |
|
| 527 |
<style scoped> |
| 528 |
.wpfnl-store-checkout-step { |
| 529 |
width: 100%; |
| 530 |
display: contents; |
| 531 |
} |
| 532 |
|
| 533 |
/* Celebration Phase */ |
| 534 |
.wpfnl-store-celebration { |
| 535 |
display: flex; |
| 536 |
align-items: center; |
| 537 |
justify-content: center; |
| 538 |
width: 100%; |
| 539 |
align-self: stretch; |
| 540 |
position: relative; |
| 541 |
} |
| 542 |
|
| 543 |
.wpfnl-confetti-canvas { |
| 544 |
position: fixed; |
| 545 |
top: 0; |
| 546 |
left: 0; |
| 547 |
width: 100%; |
| 548 |
height: 100%; |
| 549 |
pointer-events: none; |
| 550 |
z-index: 50; |
| 551 |
} |
| 552 |
|
| 553 |
.wpfnl-celebration-content { |
| 554 |
display: flex; |
| 555 |
flex-direction: column; |
| 556 |
align-items: center; |
| 557 |
text-align: center; |
| 558 |
gap: 20px; |
| 559 |
max-width: 520px; |
| 560 |
padding: 60px 40px; |
| 561 |
background: #FFFFFF; |
| 562 |
border-radius: 24px; |
| 563 |
box-shadow: 0 20px 60px rgba(110, 66, 211, 0.12), 0 8px 20px rgba(0, 0, 0, 0.06); |
| 564 |
position: relative; |
| 565 |
z-index: 100; |
| 566 |
animation: celebrationPop 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275) both; |
| 567 |
} |
| 568 |
|
| 569 |
@keyframes celebrationPop { |
| 570 |
0% { transform: scale(0.85); opacity: 0; } |
| 571 |
100% { transform: scale(1); opacity: 1; } |
| 572 |
} |
| 573 |
|
| 574 |
.wpfnl-celebration-icon { |
| 575 |
animation: iconBounce 0.8s cubic-bezier(0.175, 0.885, 0.32, 1.275) 0.3s both; |
| 576 |
} |
| 577 |
|
| 578 |
@keyframes iconBounce { |
| 579 |
0% { transform: scale(0) rotate(-20deg); opacity: 0; } |
| 580 |
60% { transform: scale(1.15) rotate(5deg); } |
| 581 |
100% { transform: scale(1) rotate(0deg); opacity: 1; } |
| 582 |
} |
| 583 |
|
| 584 |
.wpfnl-celebration-title { |
| 585 |
font-size: 28px; |
| 586 |
font-weight: 700; |
| 587 |
font-family: 'DM Sans', sans-serif; |
| 588 |
color: #363B4E; |
| 589 |
margin: 0; |
| 590 |
letter-spacing: -0.5px; |
| 591 |
} |
| 592 |
|
| 593 |
.wpfnl-celebration-subtitle { |
| 594 |
font-size: 15px; |
| 595 |
font-weight: 400; |
| 596 |
font-family: 'DM Sans', sans-serif; |
| 597 |
color: #6E7A85; |
| 598 |
line-height: 1.6; |
| 599 |
margin: 0; |
| 600 |
} |
| 601 |
|
| 602 |
.wpfnl-celebration-actions { |
| 603 |
display: flex; |
| 604 |
gap: 12px; |
| 605 |
margin-top: 8px; |
| 606 |
} |
| 607 |
|
| 608 |
.wpfnl-celebration-actions .wpfnl-mm-btn { |
| 609 |
padding: 12px 28px; |
| 610 |
border-radius: 10px; |
| 611 |
font-size: 15px; |
| 612 |
font-weight: 600; |
| 613 |
font-family: 'DM Sans', sans-serif; |
| 614 |
cursor: pointer; |
| 615 |
transition: all 0.3s ease; |
| 616 |
border: none; |
| 617 |
display: inline-flex; |
| 618 |
align-items: center; |
| 619 |
gap: 8px; |
| 620 |
height: 48px; |
| 621 |
} |
| 622 |
|
| 623 |
.wpfnl-celebration-actions .wpfnl-mm-btn-secondary { |
| 624 |
background: #F6F5FA; |
| 625 |
color: #363B4E; |
| 626 |
border: 1px solid #ECEBF0; |
| 627 |
} |
| 628 |
|
| 629 |
.wpfnl-celebration-actions .wpfnl-mm-btn-secondary:hover { |
| 630 |
border-color: #6E42D3; |
| 631 |
color: #6E42D3; |
| 632 |
} |
| 633 |
|
| 634 |
.wpfnl-celebration-actions .wpfnl-mm-btn-primary { |
| 635 |
background: #6E42D3; |
| 636 |
color: #FFFFFF; |
| 637 |
} |
| 638 |
|
| 639 |
.wpfnl-celebration-actions .wpfnl-mm-btn-primary:hover { |
| 640 |
background: #5c36b3; |
| 641 |
} |
| 642 |
|
| 643 |
/* Footer actions row (Back + Skip) */ |
| 644 |
.wpfnl-sc-footer-actions { |
| 645 |
display: flex; |
| 646 |
align-items: center; |
| 647 |
justify-content: space-between; |
| 648 |
} |
| 649 |
|
| 650 |
.wpfnl-mm-btn-skip { |
| 651 |
background: transparent; |
| 652 |
border: none; |
| 653 |
color: #9CA3AF; |
| 654 |
font-size: 14px; |
| 655 |
font-weight: 500; |
| 656 |
font-family: 'DM Sans', sans-serif; |
| 657 |
cursor: pointer; |
| 658 |
padding: 8px 16px; |
| 659 |
border-radius: 8px; |
| 660 |
transition: color 0.2s ease; |
| 661 |
} |
| 662 |
|
| 663 |
.wpfnl-mm-btn-skip:hover { |
| 664 |
color: #6E42D3; |
| 665 |
} |
| 666 |
|
| 667 |
/* Generating phase styles */ |
| 668 |
.generate-funnel-overlay { |
| 669 |
display: flex; |
| 670 |
flex-direction: column; |
| 671 |
align-items: center; |
| 672 |
justify-content: center; |
| 673 |
gap: 14px; |
| 674 |
text-align: center; |
| 675 |
} |
| 676 |
|
| 677 |
.generate-loader { |
| 678 |
order: 1; |
| 679 |
} |
| 680 |
|
| 681 |
.generated-notice { |
| 682 |
display: inline-flex; |
| 683 |
align-items: center; |
| 684 |
gap: 8px; |
| 685 |
color: #6E42D3; |
| 686 |
font-size: 14px; |
| 687 |
font-weight: 600; |
| 688 |
order: 2; |
| 689 |
} |
| 690 |
|
| 691 |
.generated-notice.is-error { |
| 692 |
color: #d14343; |
| 693 |
} |
| 694 |
</style> |
| 695 |
|