Universal ConsentFlow: Interactive Legal Disclosure & Acknowledgment Builder

Design Your Consent Flow

Live Preview

Your consent flow preview will appear here.

Generated Output

Click 'Generate Embed Code' to see the output.

`; return html; } function updatePreview() { const data = getConsentFlowData(); if (!data.flowTitle && !data.flowIntro && !data.ageQuestion && !data.dataProcessingText && !data.tosText) { livePreview.innerHTML = `

${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 })();