OnboardFlow Architect: Interactive User Tour Designer

Tour Steps Designer

Design your interactive user tour by adding steps.

Real-time Preview

See how your tour steps will look.

No steps added yet. Start designing your tour!

Code Generation

Generate the HTML, CSS, and JavaScript for your tour.

Free Use Limit Reached

You have used your 3 free attempts. Unlock unlimited access, advanced features, and optimized code generation!

Showcase Hub Bundle

Get unlimited access to this and 19+ other Pixel Office apps.

One-Time Access

Unlock this app forever with a single payment.

Crypto Micropayment

Pay securely using Solana or Bitcoin.

Sign In

`; elements.generatedCode.value = basicTourSnippet; updateGeneratedCodeDisplay(); // Update language of placeholder if empty or generated code } function updateGeneratedCodeDisplay() { if (elements.generatedCode.value.trim() === '') { elements.generatedCode.placeholder = getTranslation('generatedCodePlaceholder'); } } // WhatsApp integration for "Export Optimized Code" function exportOptimizedCodeViaWhatsApp() { if (!isUnlocked) { showPaymentOverlay(); return; } incrementUsage(); // This also counts as a "use" const tourDetails = state.tourSteps.map((step, index) => { return `--- ${getTranslation('step')} ${index + 1} ---\n` + `${getTranslation('title')}: ${step.title}\n` + `${getTranslation('message')}: ${step.message}\n` + `${getTranslation('selector')}: ${step.selector}\n`; }).join('\n'); const message = `${getTranslation('whatsappMessageBody')}${tourDetails}\n\n` + `Please provide me with the optimized, production-ready code. Thank you!`; const whatsappUrl = `https://wa.me/${WHATSAPP_NUMBER}?text=${encodeURIComponent(message)}`; window.open(whatsappUrl, '_blank'); } // --- Monetization and Firebase Logic --- function incrementUsage() { if (isUnlocked) return; actionCount++; localStorage.setItem(`pv_actions_${WIDGET_SLUG}`, actionCount.toString()); console.log(`Action count: ${actionCount}`); if (actionCount >= FREE_ACTION_LIMIT) { showPaymentOverlay(); } } function showPaymentOverlay() { elements.paymentOverlay.classList.add('active'); elements.loginModal.classList.remove('active'); // Hide login modal initially // Check if user is logged in for hub button text if (state.currentUser) { elements.hubPaymentBtn.textContent = `${getTranslation('subscribeFor')} $9/mo`; } else { elements.hubPaymentBtn.textContent = getTranslation('signInOrSubscribe'); } } function hidePaymentOverlay() { elements.paymentOverlay.classList.remove('active'); clearInterval(state.cryptoPollInterval); // Stop polling if active state.cryptoPaymentDetail.style.display = 'none'; // Hide crypto detail } function unlockWidget() { isUnlocked = true; localStorage.setItem(`pv_unlocked_${WIDGET_SLUG}`, 'true'); hidePaymentOverlay(); elements.addStepBtn.textContent = getTranslation('addStep'); // Reset button text elements.addStepBtn.disabled = false; // Enable add step button elements.exportOptimizedCodeBtn.style.display = 'inline-block'; // Show optimized export elements.exportOptimizedCodeBtn.disabled = false; } async function checkSubscription(email) { try { const response = await fetch(`https://api.pixeloffice.eu/api/pay/check-subscription?email=${encodeURIComponent(email)}`); if (!response.ok) { throw new Error('Network response was not ok'); } const data = await response.json(); if (data.active) { console.log("User has active subscription. Unlocking widget."); unlockWidget(); } else { console.log("User has no active subscription."); if (!isUnlocked) { // Only show overlay if not already unlocked showPaymentOverlay(); } } } catch (error) { console.error("Failed to check subscription:", error); // Fallback: If API fails, treat as no subscription if (!isUnlocked) { showPaymentOverlay(); } } finally { elements.authSpinner.classList.remove('active'); } } // Auth listeners auth.onAuthStateChanged(async (user) => { state.currentUser = user; if (user) { console.log("User is logged in:", user.email); elements.hubPaymentBtn.textContent = `${getTranslation('subscribeFor')} $9/mo`; // Update button text await checkSubscription(user.email); } else { console.log("User is logged out."); elements.hubPaymentBtn.textContent = getTranslation('signInOrSubscribe'); // If user logs out, and widget isn't unlocked, apply free limit logic if (!isUnlocked && actionCount >= FREE_ACTION_LIMIT) { showPaymentOverlay(); } } }); async function handleAuthSubmit() { const email = elements.authEmail.value; const password = elements.authPassword.value; elements.authErrorMessage.style.display = 'none'; elements.authSpinner.classList.add('active'); try { if (state.isLoginMode) { await auth.signInWithEmailAndPassword(email, password); elements.authErrorMessage.textContent = getTranslation('authSuccess'); elements.authErrorMessage.style.color = '#0f0'; // Neon green elements.authErrorMessage.style.display = 'block'; elements.loginModal.classList.remove('active'); } else { await auth.createUserWithEmailAndPassword(email, password); elements.authErrorMessage.textContent = getTranslation('authRegisterSuccess'); elements.authErrorMessage.style.color = '#0f0'; // Neon green elements.authErrorMessage.style.display = 'block'; state.isLoginMode = true; // Switch back to login after successful registration updateLoginModalUI(); } } catch (error) { elements.authErrorMessage.textContent = getTranslation('authError') + error.message; elements.authErrorMessage.style.color = '#f00'; // Neon red elements.authErrorMessage.style.display = 'block'; } finally { elements.authSpinner.classList.remove('active'); } } async function signInWithGoogle() { elements.authErrorMessage.style.display = 'none'; elements.authSpinner.classList.add('active'); try { const provider = new firebase.auth.GoogleAuthProvider(); await auth.signInWithPopup(provider); elements.loginModal.classList.remove('active'); } catch (error) { elements.authErrorMessage.textContent = getTranslation('authError') + error.message; elements.authErrorMessage.style.color = '#f00'; // Neon red elements.authErrorMessage.style.display = 'block'; } finally { elements.authSpinner.classList.remove('active'); } } function updateLoginModalUI() { if (state.isLoginMode) { elements.loginModalTitle.textContent = getTranslation('signIn'); elements.authSubmitBtn.textContent = getTranslation('signInButton'); elements.toggleAuthModeBtn.textContent = getTranslation('switchToRegister'); } else { elements.loginModalTitle.textContent = getTranslation('register'); elements.authSubmitBtn.textContent = getTranslation('registerButton'); elements.toggleAuthModeBtn.textContent = getTranslation('switchToSignIn'); } elements.authEmail.value = ''; elements.authPassword.value = ''; elements.authErrorMessage.style.display = 'none'; elements.authEmail.setAttribute('placeholder', getTranslation('emailPlaceholder')); elements.authPassword.setAttribute('placeholder', getTranslation('passwordPlaceholder')); } async function handleHubSubscription() { if (!state.currentUser) { elements.loginModal.classList.add('active'); return; } elements.authSpinner.classList.add('active'); // Use auth spinner for payment loading try { const response = await fetch('https://api.pixeloffice.eu/api/pay/create-hub-subscription', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: state.currentUser.email, userId: state.currentUser.uid }) }); const data = await response.json(); if (data.url) { window.location.href = data.url; } else { throw new Error(data.message || 'Failed to create Stripe Checkout session for Hub.'); } } catch (error) { console.error("Hub subscription failed:", error); alert(getTranslation('authError') + error.message); } finally { elements.authSpinner.classList.remove('active'); } } async function handleOneTimePayment() { elements.authSpinner.classList.add('active'); try { const response = await fetch('https://api.pixeloffice.eu/api/pay/create-session', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ slug: WIDGET_SLUG, amount: 199, widgetName: translations.en.widgetTitle }) }); const data = await response.json(); if (data.url) { window.location.href = data.url; } else { throw new Error(data.message || 'Failed to create Stripe Checkout session.'); } } catch (error) { console.error("One-time payment failed:", error); alert(getTranslation('authError') + error.message); } finally { elements.authSpinner.classList.remove('active'); } } async function handleCryptoPayment() { elements.authSpinner.classList.add('active'); elements.cryptoPaymentDetail.style.display = 'none'; // Hide if already visible try { const response = await fetch('https://api.pixeloffice.eu/api/pay/request-crypto', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ slug: WIDGET_SLUG, amount: 199, widgetName: translations.en.widgetTitle }) }); const data = await response.json(); if (data.qrCodeUrl && data.paymentId) { elements.cryptoQrCode.src = data.qrCodeUrl; state.cryptoPaymentId = data.paymentId; elements.cryptoPaymentDetail.style.display = 'block'; state.checkingPayment = true; startCryptoPolling(data.paymentId); } else { throw new Error(data.message || 'Failed to request crypto payment.'); } } catch (error) { console.error("Crypto payment request failed:", error); alert(getTranslation('authError') + error.message); } finally { elements.authSpinner.classList.remove('active'); } } async function startCryptoPolling(paymentId) { clearInterval(state.cryptoPollInterval); // Clear any existing interval state.cryptoPollInterval = setInterval(async () => { if (!state.checkingPayment) { clearInterval(state.cryptoPollInterval); return; } try { const response = await fetch(`https://api.pixeloffice.eu/api/pay/verify-crypto?paymentId=${paymentId}`); const data = await response.json(); if (data.status === 'confirmed') { clearInterval(state.cryptoPollInterval); state.checkingPayment = false; unlockWidget(); alert(getTranslation('authSuccess')); // Re-using authSuccess, ideally a specific payment success message } else if (data.status === 'expired') { clearInterval(state.cryptoPollInterval); state.checkingPayment = false; alert('Crypto payment expired.'); elements.cryptoPaymentDetail.style.display = 'none'; } } catch (error) { console.error("Crypto polling error:", error); // Continue polling on error, might be temporary network issue } }, 5000); // Poll every 5 seconds } function cancelCryptoPayment() { clearInterval(state.cryptoPollInterval); state.checkingPayment = false; elements.cryptoPaymentDetail.style.display = 'none'; } // Check URL for payment success on load async function checkUrlForPaymentSuccess() { const urlParams = new URLSearchParams(window.location.search); const status = urlParams.get('status'); const subscriptionStatus = urlParams.get('subscription_status'); const sessionId = urlParams.get('session_id'); if (status === 'success' && sessionId) { elements.paymentOverlay.classList.add('active'); // Show loading state elements.authSpinner.classList.add('active'); try { const response = await fetch(`https://api.pixeloffice.eu/api/pay/verify-session?session_id=${sessionId}`); const data = await response.json(); if (data.verified) { unlockWidget(); alert(getTranslation('authSuccess')); } else { throw new Error('Payment verification failed.'); } } catch (error) { console.error("Payment verification error:", error); alert(getTranslation('authError') + error.message); } finally { elements.authSpinner.classList.remove('active'); // Clear URL params const newUrl = window.location.protocol + '//' + window.location.host + window.location.pathname; window.history.replaceState({}, document.title, newUrl); } } else if (subscriptionStatus === 'success' && sessionId) { // For subscription success, Firebase auth.onAuthStateChanged will handle the check-subscription call elements.paymentOverlay.classList.add('active'); elements.authSpinner.classList.add('active'); // Wait for auth state to be resolved and subscription checked const unsubscribe = auth.onAuthStateChanged(async user => { if (user) { await checkSubscription(user.email); // Clear URL params after auth state handled const newUrl = window.location.protocol + '//' + window.location.host + window.location.pathname; window.history.replaceState({}, document.title, newUrl); } elements.authSpinner.classList.remove('active'); unsubscribe(); // Stop listening after first user state check }); } else if (!isUnlocked && actionCount >= FREE_ACTION_LIMIT) { showPaymentOverlay(); } } // Initialization document.addEventListener('DOMContentLoaded', () => { bindElements(); elements.languageSwitcher.value = currentLocale; translatePage(); // Initial translation loadSteps(); // Load existing steps if (isUnlocked) { unlockWidget(); // Ensure UI reflects unlocked state if already unlocked } else { // If not unlocked, check if free limit reached on load if (actionCount >= FREE_ACTION_LIMIT) { showPaymentOverlay(); } elements.exportOptimizedCodeBtn.disabled = true; } checkUrlForPaymentSuccess(); // Event Listeners elements.languageSwitcher.addEventListener('change', (e) => { currentLocale = e.target.value; localStorage.setItem('onboardflow_architect_locale', currentLocale); translatePage(); }); elements.addStepBtn.addEventListener('click', () => addStep()); elements.generateCodeBtn.addEventListener('click', generateTourCode); elements.exportOptimizedCodeBtn.addEventListener('click', exportOptimizedCodeViaWhatsApp); // Payment Overlay Event Listeners elements.hubPaymentBtn.addEventListener('click', handleHubSubscription); elements.oneTimePaymentBtn.addEventListener('click', handleOneTimePayment); elements.cryptoPaymentBtn.addEventListener('click', handleCryptoPayment); elements.cancelCryptoBtn.addEventListener('click', cancelCryptoPayment); // Login Modal Event Listeners elements.closeLoginModal.addEventListener('click', () => elements.loginModal.classList.remove('active')); elements.authSubmitBtn.addEventListener('click', handleAuthSubmit); elements.googleSignInBtn.addEventListener('click', signInWithGoogle); elements.toggleAuthModeBtn.addEventListener('click', () => { state.isLoginMode = !state.isLoginMode; updateLoginModalUI(); }); });