FlowCheck Pro: Interactive Process Mapper & Checklist Builder

Create Your Workflow

Design, visualize, and generate interactive, shareable process maps and checklists for any business or personal workflow.

Add New Step

Your Workflow Steps

No steps added yet. Start by adding your first step!

Free use limit reached. Unlock unlimited access!

You've reached the limit of 3 free uses. Subscribe or make a one-time purchase to continue using FlowCheck Pro without interruptions and unlock all advanced features.

FlowCheck Pro - Lifetime

Get unlimited, lifetime access to FlowCheck Pro only.

Crypto Payment

Pay securely with Solana or Bitcoin for lifetime access.

Sign In

Crypto Payment Details

Scan the QR code or copy the address to send $1.99 USD equivalent in your chosen cryptocurrency. The widget will unlock automatically upon confirmation.

`; } else { // For the basic version, we just show the inner HTML for simplicity return html; } } async function handleGenerateClick(isProFeature = false) { if (isProFeature && !isProUnlocked) { showPaymentOverlay(); return; } if (!isProUnlocked && !isProFeature) { // Only count for free version of generate let usageCount = parseInt(localStorage.getItem(USAGE_COUNT_KEY) || '0'); if (usageCount >= 3) { showPaymentOverlay(); return; } incrementUsageCount(); } const generatedContent = generateHtmlChecklist(isProFeature); generatedHtmlCodeDiv.innerHTML = `
${escapeHtml(generatedContent)}
`; checklistOutputDiv.style.display = 'block'; } function generatePdfViaWhatsapp() { if (!isProUnlocked) { showPaymentOverlay(); return; } if (steps.length === 0) { alert("Please add some steps first to generate a PDF."); return; } let message = t('whatsappMessageIntro'); steps.forEach((step, index) => { const stepNum = index + 1; message += t('whatsappMessageStep', { s: stepNum, title: step.title, instructions: step.instructions, responsible: step.responsible }) .replace("Step %s: %s", `Step ${stepNum}: ${step.title}`) .replace("Instructions: %s", `Instructions: ${step.instructions}`) .replace("Responsible: %s", `Responsible: ${step.responsible}`); }); message += t('whatsappMessageFooter'); const whatsappUrl = `https://wa.me/${WHATSAPP_NUMBER}?text=${encodeURIComponent(message)}`; window.open(whatsappUrl, '_blank'); } // --- Event Listeners for Widget Core --- addStepBtn.addEventListener('click', addStep); generateLinearBtn.addEventListener('click', () => handleGenerateClick(false)); exportFullHtmlBtn.addEventListener('click', () => handleGenerateClick(true)); generatePdfBtn.addEventListener('click', generatePdfViaWhatsapp); copyToClipboardBtn.addEventListener('click', () => { const code = generatedHtmlCodeDiv.querySelector('code').textContent; navigator.clipboard.writeText(code).then(() => { copyToClipboardBtn.textContent = t('copiedSuccess'); setTimeout(() => { copyToClipboardBtn.textContent = t('copyToClipboardBtn'); }, 2000); }).catch(err => { console.error('Failed to copy: ', err); }); }); // --- Monetization Logic --- const paymentOverlay = document.getElementById('paymentOverlay'); const paymentLoadingSpinner = document.getElementById('paymentLoadingSpinner'); const loginModalOverlay = document.getElementById('loginModalOverlay'); const authModalTitle = document.getElementById('authModalTitle'); const authErrorMessage = document.getElementById('authErrorMessage'); const authEmailInput = document.getElementById('authEmail'); const authPasswordInput = document.getElementById('authPassword'); const authSubmitBtn = document.getElementById('authSubmitBtn'); const googleSignInBtn = document.getElementById('googleSignInBtn'); const toggleAuthModeBtn = document.getElementById('toggleAuthMode'); const authLoadingSpinner = document.getElementById('authLoadingSpinner'); const hubSubscribeBtn = document.getElementById('hubSubscribeBtn'); const singlePurchaseBtn = document.getElementById('singlePurchaseBtn'); const cryptoPurchaseBtn = document.getElementById('cryptoPurchaseBtn'); const cryptoModalOverlay = document.getElementById('cryptoModalOverlay'); const qrCodeImage = document.getElementById('qrCodeImage'); const cryptoAddressSpan = document.getElementById('cryptoAddress'); const cryptoAmountP = document.getElementById('cryptoAmount'); const cryptoPaymentStatusP = document.getElementById('cryptoPaymentStatus'); const mainLoadingSpinner = document.getElementById('mainLoadingSpinner'); function showSpinner(spinnerElement, isVisible) { spinnerElement.style.display = isVisible ? 'block' : 'none'; } function showPaymentOverlay() { paymentOverlay.classList.add('visible'); updateProFeatureButtons(); updateTexts(); // Update payment overlay texts } function hidePaymentOverlay() { paymentOverlay.classList.remove('visible'); } function showLoginModal() { authErrorMessage.style.display = 'none'; loginModalOverlay.classList.add('visible'); updateTexts(); // Update login modal texts (Sign In / Register) } function closeLoginModal() { loginModalOverlay.classList.remove('visible'); } function showCryptoModal() { cryptoModalOverlay.classList.add('visible'); updateTexts(); } function closeCryptoModal() { cryptoModalOverlay.classList.remove('visible'); if (cryptoPollingInterval) { clearInterval(cryptoPollingInterval); cryptoPollingInterval = null; } showSpinner(document.getElementById('cryptoLoadingSpinner'), false); } function incrementUsageCount() { let count = parseInt(localStorage.getItem(USAGE_COUNT_KEY) || '0'); count++; localStorage.setItem(USAGE_COUNT_KEY, count.toString()); } function isUnlocked() { return isProUnlocked; } function unlockWidget() { isProUnlocked = true; localStorage.setItem(UNLOCK_KEY, "true"); hidePaymentOverlay(); closeLoginModal(); closeCryptoModal(); updateProFeatureButtons(); // alert(t('subscriptionActive')); // Or a more subtle notification } function updateProFeatureButtons() { if (isProUnlocked) { exportFullHtmlBtn.disabled = false; exportFullHtmlBtn.classList.remove('locked-feature'); generatePdfBtn.disabled = false; generatePdfBtn.classList.remove('locked-feature'); } else { exportFullHtmlBtn.disabled = true; exportFullHtmlBtn.classList.add('locked-feature'); generatePdfBtn.disabled = true; generatePdfBtn.classList.add('locked-feature'); } updateTexts(); // Ensure all localized titles are applied (especially data-i18n-text) } // --- Firebase Auth Functions --- async function handleAuth(event) { event.preventDefault(); const email = authEmailInput.value; const password = authPasswordInput.value; showSpinner(authLoadingSpinner, true); authErrorMessage.style.display = 'none'; try { if (currentAuthMode === 'signIn') { await auth.signInWithEmailAndPassword(email, password); } else { await auth.createUserWithEmailAndPassword(email, password); } // alert(t('authSuccess')); // Auth state change listener will handle further actions } catch (error) { authErrorMessage.textContent = t('authError') + error.message; authErrorMessage.style.display = 'block'; console.error("Auth error:", error); } finally { showSpinner(authLoadingSpinner, false); } } async function signInWithGoogle() { showSpinner(authLoadingSpinner, true); authErrorMessage.style.display = 'none'; try { await auth.signInWithPopup(googleProvider); // alert(t('authSuccess')); // Auth state change listener will handle further actions } catch (error) { authErrorMessage.textContent = t('authError') + error.message; authErrorMessage.style.display = 'block'; console.error("Google Auth error:", error); } finally { showSpinner(authLoadingSpinner, false); } } // Toggle sign in / sign up mode toggleAuthModeBtn.addEventListener('click', () => { currentAuthMode = currentAuthMode === 'signIn' ? 'signUp' : 'signIn'; updateTexts(); // Update button and title texts }); // --- Stripe & Crypto Payment Functions --- async function checkSubscription(email, uid) { if (!email) return false; try { // showSpinner(mainLoadingSpinner, true); // Keep spinner if already shown by auth listener const response = await fetch(`${PIXEL_OFFICE_API_BASE}/check-subscription?email=${encodeURIComponent(email)}`); const data = await response.json(); if (data.active) { unlockWidget(); return true; } return false; } catch (error) { console.error(t('subscriptionCheckError'), error); return false; } finally { // showSpinner(mainLoadingSpinner, false); // Spinner will be handled by auth listener's finalization } } async function handleHubSubscription() { if (!currentUser) { showLoginModal(); return; } showSpinner(paymentLoadingSpinner, true); try { const response = await fetch(`${PIXEL_OFFICE_API_BASE}/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 (data.url) { window.location.href = data.url; // Redirect to Stripe Checkout } else { alert(t('paymentStatusError')); } } catch (error) { console.error("Hub subscription error:", error); alert(t('paymentStatusError') + error.message); } finally { showSpinner(paymentLoadingSpinner, false); } } async function handleSinglePurchase() { showSpinner(paymentLoadingSpinner, true); try { const response = await fetch(`${PIXEL_OFFICE_API_BASE}/create-session`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ slug: WIDGET_SLUG, amount: 199, // $1.99 widgetName: t('widgetTitle') }) }); const data = await response.json(); if (data.url) { window.location.href = data.url; // Redirect to Stripe Checkout } else { alert(t('paymentStatusError')); } } catch (error) { console.error("Single purchase error:", error); alert(t('paymentStatusError') + error.message); } finally { showSpinner(paymentLoadingSpinner, false); } } async function handleCryptoPurchase() { showCryptoModal(); showSpinner(document.getElementById('cryptoLoadingSpinner'), true); try { const response = await fetch(`${PIXEL_OFFICE_API_BASE}/request-crypto`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ slug: WIDGET_SLUG, amount: 199, // $1.99 widgetName: t('widgetTitle') }) }); const data = await response.json(); if (data.qrCodeUrl && data.address && data.amountFiat && data.txId) { qrCodeImage.src = data.qrCodeUrl; qrCodeImage.style.display = 'block'; cryptoAddressSpan.textContent = data.address; cryptoAddressSpan.parentElement.display = 'flex'; // Ensure address div is visible cryptoAmountP.textContent = `Amount: ${data.amountCrypto} ${data.currency} (${data.amountFiat} USD)`; cryptoPaymentStatusP.textContent = t('paymentStatusPending'); cryptoPollingInterval = setInterval(() => pollCryptoPaymentStatus(data.txId), 5000); // Poll every 5 seconds } else { alert(t('paymentStatusError')); closeCryptoModal(); } } catch (error) { console.error("Crypto purchase error:", error); alert(t('paymentStatusError') + error.message); closeCryptoModal(); } finally { showSpinner(document.getElementById('cryptoLoadingSpinner'), false); } } async function pollCryptoPaymentStatus(txId) { try { const response = await fetch(`${PIXEL_OFFICE_API_BASE}/verify-crypto?txId=${txId}`); const data = await response.json(); if (data.status === 'completed') { cryptoPaymentStatusP.textContent = t('paymentStatusConfirmed'); clearInterval(cryptoPollingInterval); unlockWidget(); } else if (data.status === 'failed' || data.status === 'expired') { cryptoPaymentStatusP.textContent = t('paymentStatusError'); clearInterval(cryptoPollingInterval); } } catch (error) { console.error("Polling crypto payment error:", error); cryptoPaymentStatusP.textContent = t('paymentStatusError'); clearInterval(cryptoPollingInterval); } } function copyCryptoAddress() { const address = cryptoAddressSpan.textContent; navigator.clipboard.writeText(address).then(() => { alert(t('copiedSuccess')); }).catch(err => { console.error('Failed to copy crypto address: ', err); }); } // --- Initial Load & Event Handlers --- document.addEventListener('DOMContentLoaded', () => { renderSteps(); updateProFeatureButtons(); // Set initial state of Pro buttons document.getElementById('authForm').addEventListener('submit', handleAuth); document.getElementById('googleSignInBtn').addEventListener('click', signInWithGoogle); hubSubscribeBtn.addEventListener('click', handleHubSubscription); singlePurchaseBtn.addEventListener('click', handleSinglePurchase); cryptoPurchaseBtn.addEventListener('click', handleCryptoPurchase); // Check for Stripe redirect success 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) { showSpinner(mainLoadingSpinner, true); fetch(`${PIXEL_OFFICE_API_BASE}/verify-session?session_id=${sessionId}`) .then(res => res.json()) .then(data => { if (data.verified) { // alert(t('paymentVerifySuccess')); // Handled by unlockWidget indirectly unlockWidget(); } else { alert(t('paymentVerifyFailed')); } }) .catch(err => { console.error("Error verifying session:", err); alert(t('paymentVerifyFailed')); }) .finally(() => { showSpinner(mainLoadingSpinner, false); // Clean URL window.history.replaceState({}, document.title, window.location.pathname); }); } else if (subscriptionStatus === 'success' && sessionId) { // For subscription, the auth.onAuthStateChanged listener will handle the check-subscription call // and unlock logic once the user is confirmed as logged in. // We'll just clean the URL here, and the listener will take care of the rest. window.history.replaceState({}, document.title, window.location.pathname); } // Check initial usage count for the free version if (!isProUnlocked && parseInt(localStorage.getItem(USAGE_COUNT_KEY) || '0') >= 3) { showPaymentOverlay(); } }); // Firebase Auth State Listener auth.onAuthStateChanged(async user => { currentUser = user; // Store the user object globally showSpinner(mainLoadingSpinner, false); // Hide main loading spinner once auth state is known if (user) { // User is signed in. Check subscription. await checkSubscription(user.email, user.uid); closeLoginModal(); // Close login modal if user signs in successfully } else { // User is signed out. // If the widget was unlocked by subscription (and not by single purchase, which is persistent) // it should lock again. // localStorage.getItem(UNLOCK_KEY) === "true" indicates a permanent single purchase unlock. // If isProUnlocked is true, but it's not a permanent local unlock, then it must be from subscription. if (isProUnlocked && localStorage.getItem(UNLOCK_KEY) !== "true") { isProUnlocked = false; // Reset if unlocked only by subscription (not persistent local flag) updateProFeatureButtons(); } } updateTexts(); // Re-apply texts after auth state might have changed button labels }); // Initial render and text update renderSteps(); updateTexts();