PluginProbe
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell / 3.10.8
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell v3.10.8
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 / StoreCheckout.vue

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

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