From e57fee8a88a9950451bbd1b861525642ef39303f Mon Sep 17 00:00:00 2001 From: Dorian Date: Fri, 15 May 2026 14:35:25 -0500 Subject: [PATCH] Persist signup progress through PWA install --- public/sw.js | 10 ++------ server/server.js | 7 ++++- src/App.vue | 66 +++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 73 insertions(+), 10 deletions(-) diff --git a/public/sw.js b/public/sw.js index bd21f9d..a2bc3a5 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,13 +1,7 @@ -const CACHE_NAME = 'l484-pwa-v5' +const CACHE_NAME = 'l484-pwa-v6' const APP_SHELL = [ '/', '/manifest.webmanifest', - '/images/app-icon-192.png', - '/images/app-icon-512.png', - '/images/apple-touch-icon.png', - '/images/small-logo.svg', - '/images/header-logo.svg', - '/images/pattern.jpg', ] self.addEventListener('install', (event) => { @@ -45,7 +39,7 @@ self.addEventListener('fetch', (event) => { event.respondWith( caches.match(event.request).then((cached) => cached || fetch(event.request).then((response) => { - if (event.request.method === 'GET' && response.ok) { + if (event.request.method === 'GET' && response.ok && response.type === 'basic') { const clone = response.clone() caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone)) } diff --git a/server/server.js b/server/server.js index 105ce7e..4d29fce 100644 --- a/server/server.js +++ b/server/server.js @@ -1,6 +1,6 @@ import crypto from 'node:crypto' import fs from 'node:fs/promises' -import { existsSync, createReadStream } from 'node:fs' +import { existsSync, createReadStream, statSync } from 'node:fs' import http from 'node:http' import path from 'node:path' import { fileURLToPath } from 'node:url' @@ -472,7 +472,12 @@ const serveStatic = (req, res) => { const headers = { 'Content-Type': types[ext] || 'application/octet-stream' } if (path.basename(safePath) === 'sw.js' || ext === '.html' || ext === '.webmanifest') { headers['Cache-Control'] = 'no-store' + } else if (safePath.startsWith(path.join(distDir, 'assets'))) { + headers['Cache-Control'] = 'public, max-age=31536000, immutable' + } else { + headers['Cache-Control'] = 'public, max-age=3600' } + headers['Content-Length'] = statSync(safePath).size res.writeHead(200, headers) createReadStream(safePath).pipe(res) } diff --git a/src/App.vue b/src/App.vue index de12479..86b3680 100644 --- a/src/App.vue +++ b/src/App.vue @@ -107,6 +107,7 @@ const ADMIN_AUTH_KEY = 'l484-admin-user' const USER_ID_KEY = 'l484-user-id' const SIGNER_LOGIN_COMPLETE_KEY = 'l484-signer-login-complete' const SIGNER_LOGIN_CONTEXT_KEY = 'l484-signer-login-context' +const SIGNUP_DRAFT_KEY = 'l484-signup-draft' const MAX_NAME_LENGTH = 80 const MAX_EMAIL_LENGTH = 160 const MAX_PHONE_LENGTH = 32 @@ -719,6 +720,50 @@ const saveMembers = () => { localStorage.setItem(MEMBERS_KEY, JSON.stringify(members.value)) } +const clampSignupStep = (step) => { + const numericStep = Number(step) + if (!Number.isFinite(numericStep)) return 0 + return Math.min(5, Math.max(0, Math.round(numericStep))) +} + +const saveSignupDraft = () => { + if (currentMember.value || signupStep.value === 5) return + localStorage.setItem(SIGNUP_DRAFT_KEY, JSON.stringify({ + step: clampSignupStep(signupStep.value), + form: { + fullName: form.fullName, + email: form.email, + phone: form.phone, + accepted: form.accepted, + signature: form.signature, + }, + savedAt: Date.now(), + })) +} + +const clearSignupDraft = () => { + localStorage.removeItem(SIGNUP_DRAFT_KEY) +} + +const restoreSignupDraft = () => { + try { + const draft = JSON.parse(localStorage.getItem(SIGNUP_DRAFT_KEY) || 'null') + if (!draft || typeof draft !== 'object') return 0 + form.fullName = sanitizeText(draft.form?.fullName, MAX_NAME_LENGTH) + form.email = sanitizeText(draft.form?.email, MAX_EMAIL_LENGTH) + form.phone = sanitizeText(draft.form?.phone, MAX_PHONE_LENGTH) + form.accepted = Boolean(draft.form?.accepted) + form.signature = DATA_IMAGE_PATTERN.test(draft.form?.signature || '') ? draft.form.signature : '' + signatureHasInk.value = Boolean(form.signature) + const restoredStep = clampSignupStep(draft.step) + if (restoredStep === 1 && isPwaStandalone.value) return 2 + return restoredStep + } catch { + clearSignupDraft() + return 0 + } +} + const detectInstallPlatform = () => { const ua = navigator.userAgent || '' const isIos = /iphone|ipad|ipod/i.test(ua) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1) @@ -744,6 +789,9 @@ const handlePwaInstalled = () => { deferredInstallPrompt.value = null pwaInstallMessage.value = 'L484 is installed. Continue signup from the app.' refreshPwaStandalone() + if (isSignupOpen.value && signupStep.value === 1) { + signupStep.value = 2 + } } const handlePwaInstallPrimary = async () => { @@ -785,8 +833,8 @@ const openSignup = () => { loadBitcoinPrice() refreshPwaStandalone() installPlatform.value = detectInstallPlatform() - signupStep.value = currentMember.value ? 5 : 0 createdMember.value = currentMember.value + signupStep.value = currentMember.value ? 5 : restoreSignupDraft() isCardRevealing.value = false generatedCredentials.value = null formError.value = '' @@ -813,6 +861,7 @@ const toggleMobileMenu = () => { } const closeSignup = () => { + saveSignupDraft() isSignupOpen.value = false isCardRevealing.value = false generatedCredentials.value = null @@ -829,6 +878,7 @@ const signOutMember = () => { clearSigner() cancelPendingRemoteAppLogin() localStorage.removeItem('l484-member-keys') + clearSignupDraft() currentMemberId.value = '' createdMember.value = null generatedCredentials.value = null @@ -1023,6 +1073,7 @@ const createMembership = async () => { localStorage.setItem(CURRENT_MEMBER_KEY, savedMember.membershipId) localStorage.setItem(USER_ID_KEY, savedMember.userId) saveMembers() + clearSignupDraft() resetForm() isCardRevealing.value = true signupStep.value = 5 @@ -1052,6 +1103,14 @@ const syncSignatureCanvas = () => { context.lineJoin = 'round' context.lineWidth = 2.4 context.strokeStyle = '#ffffff' + + if (DATA_IMAGE_PATTERN.test(form.signature)) { + const savedSignature = new Image() + savedSignature.onload = () => { + context.drawImage(savedSignature, 0, 0, rect.width, rect.height) + } + savedSignature.src = form.signature + } } const getSignaturePoint = (event) => { @@ -2069,11 +2128,16 @@ onBeforeUnmount(() => { }) watch(signupStep, async (step) => { + saveSignupDraft() if (step !== 4) return await nextTick() syncSignatureCanvas() }) +watch(form, () => { + if (isSignupOpen.value) saveSignupDraft() +}, { deep: true }) + watch([adminActionMessage, adminActionError], ([message, error]) => { window.clearTimeout(adminToastTimer) if (!message && !error) return