Design Your Consent Flow
Live Preview
Your consent flow preview will appear here.
Generated Output
Click 'Generate Embed Code' to see the output.
Your consent flow preview will appear here.
Click 'Generate Embed Code' to see the output.
You've used your 3 free actions. Choose an option below to continue building amazing consent flows without limits.
Scan the QR code below or send the exact amount to the address.
Amount:
Address:
Network:
${i18n[currentLang].previewPlaceholder}
`; return; } livePreview.innerHTML = generateConsentFlowHTML(data); } function generateCode() { const data = getConsentFlowData(); const fullCode = generateConsentFlowHTML(data); generatedCode.textContent = fullCode; generatedCodePlaceholder.style.display = 'none'; } async function copyCode() { try { await navigator.clipboard.writeText(generatedCode.textContent); alert(i18n[currentLang].copiedToClipboard); } catch (err) { console.error('Failed to copy: ', err); } } function exportJson() { const data = getConsentFlowData(); const jsonData = JSON.stringify(data, null, 2); const blob = new Blob([jsonData], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'consentflow_config.json'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } function sendViaWhatsApp() { const data = getConsentFlowData(); const message = `${i18n[currentLang].whatsappMessageSubject}\n\n` + `${i18n[currentLang].whatsappMessagePrefix} Flow Title: ${data.flowTitle || 'N/A'} Intro Text: ${data.flowIntro || 'N/A'} Age Question: ${data.ageQuestion || 'N/A'} Age Deny Redirect: ${data.ageDenyRedirect || 'N/A'} Data Processing Text: ${data.dataProcessingText || 'N/A'} Data Processing Consent Label: ${data.dataProcessingConsentLabel || 'N/A'} ToS Text: ${data.tosText || 'N/A'} ToS Consent Label: ${data.tosConsentLabel || 'N/A'} ${i18n[currentLang].whatsappMessageSuffix}`; window.open(`https://wa.me/${WHATSAPP_NUMBER}?text=${encodeURIComponent(message)}`, '_blank'); } // --- Payment & Access Logic --- function showLoading(element, show) { if (show) { element.classList.add('active'); } else { element.classList.remove('active'); } } function showPaymentOverlay() { if (isWidgetUnlocked) return; // Don't show if already unlocked paymentOverlay.classList.add('visible'); // Ensure login modal or crypto modal is hidden when main overlay is shown hideLoginModal(); hideCryptoModal(); } function hidePaymentOverlay() { paymentOverlay.classList.remove('visible'); hideLoginModal(); hideCryptoModal(); } function showLoginModal() { loginModal.classList.add('visible'); authErrorMessage.textContent = ''; } function hideLoginModal() { loginModal.classList.remove('visible'); authErrorMessage.textContent = ''; } function showCryptoModal() { cryptoModal.classList.add('visible'); cryptoStatusMessage.textContent = i18n[currentLang].cryptoStatusPending; cryptoStatusMessage.style.color = var(--text-color); // Reset color } function hideCryptoModal() { cryptoModal.classList.remove('visible'); } async function unlockWidget() { isWidgetUnlocked = true; localStorage.setItem(`pv_unlocked_${WIDGET_SLUG}`, "true"); hidePaymentOverlay(); // Reset usage count if unlocked later localStorage.setItem(`pv_actions_${WIDGET_SLUG}`, '0'); usageCount = 0; } async function checkSubscriptionStatus(email) { try { const response = await fetch(`${API_BASE_URL}/check-subscription?email=${encodeURIComponent(email)}`); if (!response.ok) throw new Error('Network response was not ok.'); const data = await response.json(); return data.active; } catch (error) { console.error("Error checking subscription status:", error); return false; } } async function incrementUsage(actionCallback) { if (isWidgetUnlocked) { actionCallback(); return; } usageCount++; localStorage.setItem(`pv_actions_${WIDGET_SLUG}`, usageCount); if (usageCount <= 3) { actionCallback(); } else { showPaymentOverlay(); } } // --- Firebase Auth Functions --- async function handleEmailAuth() { const email = authEmailInput.value; const password = authPasswordInput.value; if (!email || !password) { authErrorMessage.textContent = i18n[currentLang].authError + "Email and password cannot be empty."; return; } showLoading(paymentSpinner, true); authErrorMessage.textContent = ''; try { if (isLoginMode) { await auth.signInWithEmailAndPassword(email, password); } else { await auth.createUserWithEmailAndPassword(email, password); } // User is signed in, onAuthStateChanged will handle the rest } catch (error) { authErrorMessage.textContent = i18n[currentLang].authError + error.message; } finally { showLoading(paymentSpinner, false); } } async function handleGoogleSignIn() { showLoading(paymentSpinner, true); authErrorMessage.textContent = ''; try { await auth.signInWithPopup(googleProvider); // User is signed in, onAuthStateChanged will handle the rest } catch (error) { authErrorMessage.textContent = i18n[currentLang].authError + error.message; } finally { showLoading(paymentSpinner, false); } } function toggleAuthMode() { isLoginMode = !isLoginMode; emailAuthBtn.textContent = isLoginMode ? i18n[currentLang].signInBtn : i18n[currentLang].registerBtn; toggleAuthModeBtn.textContent = isLoginMode ? i18n[currentLang].toggleToRegister : i18n[currentLang].toggleToSignIn; loginModalTitle.textContent = isLoginMode ? i18n[currentLang].loginModalTitle : i18n[currentLang].loginModalTitle.replace('Sign In', 'Register').replace('Přihlásit se', 'Registrovat').replace('Anmelden', 'Registrieren').replace('Se connecter', 'S\'inscrire').replace('Iniciar sesión', 'Registrarse').replace('Accedi', 'Registrati').replace('Zaloguj się', 'Zarejestruj'); authErrorMessage.textContent = ''; } // --- Payment API Calls --- async function handleHubSubscription() { if (!currentUser) { showLoginModal(); return; } showLoading(paymentSpinner, true); try { const response = await fetch(`${API_BASE_URL}/create-hub-subscription`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: currentUser.email, userId: currentUser.uid }) }); const data = await response.json(); if (response.ok && data.url) { window.location.href = data.url; } else { alert('Failed to initiate subscription: ' + (data.error || 'Unknown error.')); } } catch (error) { console.error("Error creating Hub subscription:", error); alert('An error occurred while initiating your subscription.'); } finally { showLoading(paymentSpinner, false); } } async function handleOneTimePayment() { showLoading(paymentSpinner, true); try { const response = await fetch(`${API_BASE_URL}/create-session`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ slug: WIDGET_SLUG, amount: 199, // $1.99 in cents widgetName: i18n[currentLang].appTitle }) }); const data = await response.json(); if (response.ok && data.url) { window.location.href = data.url; } else { alert('Failed to initiate payment: ' + (data.error || 'Unknown error.')); } } catch (error) { console.error("Error creating Stripe session:", error); alert('An error occurred while initiating your payment.'); } finally { showLoading(paymentSpinner, false); } } let cryptoPollingInterval; async function handleCryptoPayment() { showLoading(paymentSpinner, true); try { const response = await fetch(`${API_BASE_URL}/request-crypto`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ slug: WIDGET_SLUG, amount: 199, // $1.99 in cents widgetName: i18n[currentLang].appTitle }) }); const data = await response.json(); if (response.ok && data.address && data.amount && data.network && data.paymentId) { cryptoAmountValue.textContent = `${(data.amount / 100).toFixed(2)} USD`; // Assuming amount is in cents cryptoAddressValue.textContent = data.address; cryptoNetworkValue.textContent = data.network; cryptoQrCode.src = `https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(data.address)}`; showCryptoModal(); pollCryptoPayment(data.paymentId); } else { alert('Failed to initiate crypto payment: ' + (data.error || 'Unknown error.')); } } catch (error) { console.error("Error requesting crypto payment:", error); alert('An error occurred while initiating your crypto payment.'); } finally { showLoading(paymentSpinner, false); } } function pollCryptoPayment(paymentId) { cryptoPollingInterval = setInterval(async () => { try { const response = await fetch(`${API_BASE_URL}/verify-crypto?paymentId=${paymentId}`); const data = await response.json(); if (data.status === 'paid') { clearInterval(cryptoPollingInterval); cryptoStatusMessage.textContent = i18n[currentLang].cryptoStatusSuccess; cryptoStatusMessage.style.color = var(--success-color); unlockWidget(); setTimeout(hideCryptoModal, 3000); } else if (data.status === 'expired' || data.status === 'failed') { clearInterval(cryptoPollingInterval); cryptoStatusMessage.textContent = i18n[currentLang].cryptoError; cryptoStatusMessage.style.color = var(--error-color); } } catch (error) { console.error("Error polling crypto payment:", error); clearInterval(cryptoPollingInterval); cryptoStatusMessage.textContent = i18n[currentLang].cryptoError; cryptoStatusMessage.style.color = var(--error-color); } }, 5000); // Poll every 5 seconds } // --- Event Listeners --- previewFlowBtn.addEventListener('click', updatePreview); generateCodeBtn.addEventListener('click', () => incrementUsage(generateCode)); copyCodeBtn.addEventListener('click', () => incrementUsage(copyCode)); exportJsonBtn.addEventListener('click', () => incrementUsage(exportJson)); whatsappBtn.addEventListener('click', () => incrementUsage(sendViaWhatsApp)); // Input change listeners for live preview document.querySelectorAll('.builder-section input, .builder-section textarea').forEach(input => { input.addEventListener('input', updatePreview); }); // Payment overlay event listeners hubSubscribeBtn.addEventListener('click', handleHubSubscription); oneTimeBuyBtn.addEventListener('click', handleOneTimePayment); cryptoPayBtn.addEventListener('click', handleCryptoPayment); loginCloseBtn.addEventListener('click', hideLoginModal); cryptoCloseBtn.addEventListener('click', () => { hideCryptoModal(); clearInterval(cryptoPollingInterval); // Stop polling when modal is closed }); emailAuthBtn.addEventListener('click', handleEmailAuth); googleSigninBtn.addEventListener('click', handleGoogleSignIn); toggleAuthModeBtn.addEventListener('click', toggleAuthMode); // --- Initial Load & Firebase Auth Check --- auth.onAuthStateChanged(async (user) => { currentUser = user; if (user) { // User is signed in. Check subscription. const hasSubscription = await checkSubscriptionStatus(user.email); if (hasSubscription) { unlockWidget(); } else { // User logged in but no active subscription for Hub hubSubscribeBtn.textContent = i18n[currentLang].hubSubscribeBtnLoggedIn; } hideLoginModal(); // Hide login modal if user logs in } else { // User is signed out. isWidgetUnlocked = localStorage.getItem(`pv_unlocked_${WIDGET_SLUG}`) === "true"; // Re-check local storage hubSubscribeBtn.textContent = i18n[currentLang].hubSubscribeBtn; if (!isWidgetUnlocked && usageCount > 3) { showPaymentOverlay(); // Show overlay if limit exceeded and not unlocked } } updateTexts(); // Update texts based on login status }); // --- URL Parameter Handling for Stripe Success --- async function handleUrlParams() { 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) { showLoading(paymentSpinner, true); paymentOverlay.classList.add('visible'); // Show overlay with spinner while verifying try { const response = await fetch(`${API_BASE_URL}/verify-session?session_id=${sessionId}`); const data = await response.json(); if (response.ok && data.verified) { unlockWidget(); } else { alert('Payment verification failed.'); console.error('Payment verification failed:', data); } } catch (error) { console.error('Error verifying Stripe session:', error); alert('An error occurred during payment verification.'); } finally { showLoading(paymentSpinner, false); history.replaceState(null, '', window.location.pathname); // Clean URL } } else if (subscriptionStatus === 'success' && sessionId) { showLoading(paymentSpinner, true); paymentOverlay.classList.add('visible'); // Show overlay with spinner while verifying // For subscription, we rely on auth.onAuthStateChanged to re-check the subscription // after successful payment redirection. The user should ideally be logged in at this point. // We'll give Firebase listener a moment to pick it up, then check. await new Promise(resolve => setTimeout(resolve, 2000)); // Give Firebase a moment if (auth.currentUser) { const hasSubscription = await checkSubscriptionStatus(auth.currentUser.email); if (hasSubscription) { unlockWidget(); } else { alert('Subscription verification pending or failed. Please check your account.'); } } else { alert('Subscription payment successful. Please sign in to verify access.'); showPaymentOverlay(); showLoginModal(); } showLoading(paymentSpinner, false); history.replaceState(null, '', window.location.pathname); // Clean URL } } // --- Initializations --- (async function init() { // Set default language and update texts updateTexts(); // Check existing unlocked status from localStorage first (for quick reload) if (isWidgetUnlocked) { // No need to show overlay or apply limits } else { // Check if URL contains success params from Stripe await handleUrlParams(); // If not unlocked by URL params (or if not applicable), and usage limit exceeded, show overlay if (!isWidgetUnlocked && usageCount > 3) { showPaymentOverlay(); } } updatePreview(); // Initial preview render })();