Persist signup progress through PWA install

This commit is contained in:
Dorian
2026-05-15 14:35:25 -05:00
parent be69dd97e7
commit e57fee8a88
3 changed files with 73 additions and 10 deletions

View File

@@ -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))
}

View File

@@ -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)
}

View File

@@ -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