Persist signup progress through PWA install
This commit is contained in:
10
public/sw.js
10
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))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
66
src/App.vue
66
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
|
||||
|
||||
Reference in New Issue
Block a user