const urlRegex = /(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})/; const textMap = { url: 'Type in a website URL below', googleForm: 'Type in a URL for your Google Form', pdf: 'Type in a URL for your PDF file ', googleReview: 'Type in a URL for your Google Reviews page', yelpReview: 'Enter the link to your Yelp Reviews page', eventPage: 'Type in a URL for your event page', restaurant: 'Type in a URL for your menu', realEstate: 'Type in a URL for your listing or agency', appStore: 'Type in a URL for your app listing', mp3: 'Type in a URL for your mp3 file', images: 'Type in an URL for your image or album', video: 'Type in a URL for your video ', payment: 'Type in a URL for your payment page', googleMaps: 'Type in a URL for your Google Maps location', bulkUpload: 'Enter the link to your CSV file', couponCodes: 'Type in a URL for your coupon code', }; const validTypes = [ 'vcard', 'socialMedia', 'eventPage', ...Object.keys(textMap), ]; const initialState = { qrType: 'url', trackScans: false, updateLater: false, url: '', color: '#000000', email: '', template: 'default', style: 'default', dedicatedForm: '', }; const handler = { get(target, key) { if (typeof target[key] === 'object' && target[key] !== null) { return new Proxy(target[key], handler); } return target[key]; }, set(target, prop, value) { if (prop === 'email') { document.querySelectorAll('#social_submit, #vcard_submit').forEach(el => { if (value.trim()) { el.classList.remove('disabled'); } else { el.classList.add('disabled'); } }); } if (prop === 'url') { const el = document.getElementById('downloadPNG'); if (el) { if (value.trim() && urlRegex.test(value.trim())) { el.classList.remove('disabled'); } else { el.classList.add('disabled'); } } } if (prop === 'color') { window.updateState({ innerColor: target.url && target.url.trim() ? value : '#DDDDDD', outerColor: target.url && target.url.trim() ? value : '#DDDDDD', }); } if (prop === 'style') { window.updateState({ selectedStyle: value, }); } if (prop === 'qrType') { document.querySelectorAll("input[type='email']").forEach(el => { el.value = target.email; }); try { document.querySelector('a.tab_link.w--current, a.more_links.w--current').classList.remove('w--current'); } catch (e) { console.log('failed to get w--current link'); } try { Array.from(document.querySelectorAll('a.tab_link, a.more_links')).find(el => el.id === value).classList.add('w--current'); } catch (e) { console.log('Failed to set w--current link'); } try { document.querySelector('#form > .c_form_row > h2').textContent = textMap[value]; } catch (e) { console.log('Failed to set h2 content'); } document.querySelectorAll('#flowQR,#flowVcard,#flowSocial').forEach(el => { el.classList.add('hide'); if (value === 'vcard' && el.id === 'flowVcard') { el.classList.remove('hide'); } if (value === 'socialMedia' && el.id === 'flowSocial') { el.classList.remove('hide'); } if (![ 'socialMedia', 'vcard' ].includes(value) && el.id === 'flowQR') { el.classList.remove('hide'); } }); // close the dropdown document.body.dispatchEvent(new Event('mousedown', { bubbles: true })); document.body.dispatchEvent(new Event('mouseup', { bubbles: true })); document.body.dispatchEvent(new Event('click', { bubbles: true })); try { document.getElementById('text').focus(); } catch (e) { console.log('missing text element'); } } target[prop] = value; }, }; const formState = new Proxy(initialState, handler); (function () { const params = new URLSearchParams(document.location.search); const { pathname } = document.location; const type = params.get('type'); if (type && validTypes.includes(type)) { formState.qrType = type; } if (pathname === '/virtual-business-cards') { formState.qrType = 'vcard'; } if (pathname === '/google-reviews') { formState.qrType = 'googleReview'; } }()); document.querySelectorAll('a.tab_link, a.more_links').forEach(link => { if (link.id === formState.qrType) { link.classList.add('w--current'); } link.addEventListener('click', evt => { evt.preventDefault(); evt.stopPropagation(); formState.qrType = link.id; formState.dedicatedForm = ''; }); }); document.querySelectorAll('#text').forEach(el => { el.addEventListener('input', ({ target }) => { const { value } = target; formState.url = value; formState.dedicatedForm = ''; window.updateState({ value: value.trim() || 'www.sqr.me/qr-code-generator', innerColor: value && value.trim() ? formState.color : '#DDDDDD', outerColor: value && value.trim() ? formState.color : '#DDDDDD', }); try { target.parentNode.querySelector('.error').classList.add('hide'); } catch (e) { console.log('failed to clear error. [invalid URL]'); } }); el.addEventListener('blur', ({ target }) => { const { value } = target; const valid = !value || urlRegex.test(value) || 'Please provide a valid URL.'; if (valid !== true) { try { const errorDiv = target.parentNode.querySelector('.error'); errorDiv.textContent = valid; errorDiv.classList.remove('hide'); } catch (e) { alert(valid); console.log('failed to set error. [invalid URL]'); } } }); }); document.querySelectorAll("[name='style']").forEach(el => { el.addEventListener('click', ({ target }) => { const { value } = target; formState.style = value; }); }); document.querySelectorAll("[name='Color']").forEach(el => { el.addEventListener('click', ({ target }) => { const { value } = target; formState.color = value; }); }); try { document.getElementById('uploadPDF').addEventListener('click', () => { formState.dedicatedForm = 'uploadPDF'; }); } catch (e) { console.log('missing uploadPDF'); } try { document.getElementById('checkbox').addEventListener('click', ({ target }) => { const { checked } = target; formState.trackScans = checked; }); formState.trackScans = document.getElementById('checkbox').checked; } catch (e) { console.log('missing checkbox'); } try { document.getElementById('checkbox-2').addEventListener('click', ({ target }) => { const { checked } = target; formState.updateLater = checked; }); formState.updateLater = document.getElementById('checkbox-2').checked; } catch (e) { console.log('missing checkbox-2'); } document.querySelectorAll("input[type='email']").forEach(el => { el.addEventListener('input', ({ target }) => { const { value } = target; formState.email = value; }); }); const signupForm = document.getElementById('signup'); if (signupForm) { signupForm.addEventListener('submit', e => { e.preventDefault(); e.stopPropagation(); register(); }); } const emailForm = document.getElementById('email-form'); if (emailForm) { emailForm.addEventListener('submit', e => { e.preventDefault(); e.stopPropagation(); register(); }); const continueBtn = emailForm.querySelector("input[type='submit']"); emailForm.addEventListener('input', () => { if (continueBtn) { if (formState.email && formState.email.trim()) { continueBtn.classList.remove('disabled'); continueBtn.removeAttribute('disabled'); } else { continueBtn.classList.add('disabled'); continueBtn.setAttribute('disabled', ''); } } }); } document.querySelectorAll('.c_vard_template_selector_item').forEach(el => { el.addEventListener('click', evt => { evt.preventDefault(); evt.stopPropagation(); formState.template = el.id; const preview = document.querySelector('.vcard_preview'); preview.className = `vcard_preview template-${el.id}`; const active = document.querySelector('.c_vard_template_selector_item.active'); if (active) { active.classList.remove('active'); } el.classList.add('active'); }); }); function getType() { return ( formState.trackScans || formState.updateLater || [ 'vcard', 'socialMedia', 'eventPage' ].includes(formState.qrType) ) || (document.querySelector('input#dyn') || { checked: false }).checked ? 'dynamic' : 'static'; } async function register() { const { pathname } = document.location; const submit = document.querySelector("#signup input[type='submit']"); const errorDiv = document.getElementById('signup-error'); const needsUrl = ![ 'vcard', 'socialMedia' ].includes(formState.qrType); const dedicatedUrls = [ '/virtual-business-cards', '/google-reviews', '/qr-code-creator', '/qr-code-generator-z7' ]; const dedicatedForm = dedicatedUrls.includes(pathname) ? pathname : formState.dedicatedForm; if (formState.email && formState.color && (formState.url || !needsUrl || dedicatedForm)) { try { submit.value = submit.dataset.wait; submit.setAttribute('disabled', true); } catch (e) { console.log('failed to set submit text'); } try { document.querySelectorAll('#social_submit, #vcard_submit').forEach(el => { el.setAttribute('data-default-text', el.textContent); el.textContent = 'Please wait...'; }); } catch (e) { console.log('failed to set submit text #2'); } try { errorDiv.style.setProperty('display', 'none'); errorDiv.textContent = ''; } catch (e) { console.log('failed to set error text'); } try { document.querySelectorAll('#flowVcard .w-form-fail, #flowSocial .w-form-fail').forEach(el => { el.style.setProperty('display', 'none'); el.textContent = ''; }); } catch (e) { console.log('failed to set error text #2'); } window.sona.identify({ email: formState.email, }); const formData = { type: getType(), qrtype: formState.qrType, text: formState.url, color: formState.color, style: formState.style, trackScans: formState.trackScans, updateLater: formState.updateLater, }; window.sona.formData(formData); window.sona.event('Form Submit'); window.sona.pageview(); try { sha256(formState.email).then(digest => { window.amplitude.setUserId(digest); window.amplitude.track('Form Submit', { pathname, formData, }); }); } catch (e) { console.error('Failed to track event.', e); } const data = { state: { qrtype: formState.qrType, value: formState.url, color: formState.color, type: getType(), template: formState.template, selectedStyle: formState.style, dedicatedForm, }, email: formState.email, }; const apiUrl = 'https://api.sqr.me/api'; const registerResult = await fetch(`${apiUrl}/register`, { headers: { Accept: 'application/json', 'Content-Type': 'application/json', }, method: 'POST', body: JSON.stringify({ email: formState.email }), }).then(r => r.json()).then(r => r); // we only get a token when a new user registers. // otherwise it is considered a "login" if (registerResult.status === 'success' && registerResult.token) { await fetch(`${apiUrl}/settings`, { headers: { Accept: 'application/json', 'Content-Type': 'application/json', Authorization: `Bearer ${registerResult.token}`, }, method: 'POST', body: JSON.stringify({ name: 'externalData', value: JSON.stringify(data.state) }), }); } if (registerResult.status === 'success') { const { search } = window.location; const redir = `https://app.sqr.me/external-link?data=${btoa(JSON.stringify({ email: data.email }))}${search ? `&${search.replace('?', '')}` : ''}`; try { window.gtag_report_conversion(redir); setTimeout(() => { window.location.href = redir; }, 1000); } catch (e) { window.location.href = redir; } } if (registerResult.status === 'error') { try { errorDiv.style.setProperty('display', 'block'); errorDiv.textContent = registerResult.message; } catch (e) { window.alert(registerResult.message); } try { document.querySelectorAll('#flowVcard .w-form-fail, #flowSocial .w-form-fail').forEach(el => { el.style.setProperty('display', 'block'); el.textContent = registerResult.message; }); } catch (e) { console.log('failed to set error text #3'); } } submit.removeAttribute('disabled'); } else { try { errorDiv.style.setProperty('display', 'block'); errorDiv.textContent = 'Please fill out the form before submit.'; } catch (e) { console.log('missing data'); } } } try { document.getElementById('downloadPNG').addEventListener('click', evt => { if (!formState.url || !urlRegex.test(formState.url)) { evt.stopPropagation(); evt.preventDefault(); return; } window.scrollTo({ top: 0, left: 0, }); }); } catch (e) { console.log('missing downloadPNG'); } try { document.getElementById('downloadSVG').addEventListener('click', evt => { if (!formState.url || !urlRegex.test(formState.url)) { evt.stopPropagation(); evt.preventDefault(); return; } window.scrollTo({ top: 0, left: 0, }); }); } catch (e) { console.log('missing downloadSVG'); } document.querySelectorAll('#social_submit, #vcard_submit').forEach(el => { el.addEventListener('click', () => { register(); }); }); document.querySelectorAll('input[type="text"]').forEach(el => { el.addEventListener('keypress', evt => { if (evt.keyCode === 13) { evt.preventDefault(); evt.stopPropagation(); } }); }); // Computes the SHA-256 digest of a string with Web Crypto // Source: https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest function sha256(str) { // Get the string as arraybuffer. const buffer = new TextEncoder('utf-8').encode(str); return crypto.subtle.digest('SHA-256', buffer).then(hash => hex(hash)); } function hex(buffer) { let digest = ''; const view = new DataView(buffer); for (let i = 0; i < view.byteLength; i += 4) { // We use getUint32 to reduce the number of iterations (notice the `i += 4`) const value = view.getUint32(i); // toString(16) will transform the integer into the corresponding hex string // but will remove any initial "0" const stringValue = value.toString(16); // One Uint32 element is 4 bytes or 8 hex chars (it would also work with 4 // chars for Uint16 and 2 chars for Uint8) const padding = '00000000'; const paddedValue = (padding + stringValue).slice(-padding.length); digest += paddedValue; } return digest; } // Should output "c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2" // We can check the result with: // python -c 'from hashlib import sha256;print sha256("foobar").hexdigest()' // sha256("foobar").then(function(digest) { // console.log(digest) // }) //