Photo ID upload: add QR code for phone + scanner device support

Three upload methods:
- Upload File: standard file picker
- Camera / Scanner: uses capture attribute for camera on mobile
  or TWAIN/WIA scanner devices on desktop
- QR Code: generates QR with current page URL so user can scan
  with phone and take a photo of their ID on mobile

QR generated via api.qrserver.com (no library dependency).
Remove button restores all upload options.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
justin 2026-05-30 15:56:00 -05:00
parent b8a52303b3
commit e8769e4d5d

View file

@ -214,10 +214,34 @@
<img id="dot-id-img" style="max-width:200px;max-height:150px;border-radius:6px;border:1px solid #d1d5db" />
<button type="button" id="dot-id-remove" style="background:#fee2e2;color:#991b1b;border:none;padding:4px 12px;border-radius:4px;font-size:12px;cursor:pointer">Remove</button>
</div>
<button type="button" id="dot-id-btn" style="display:flex;flex-direction:column;align-items:center;gap:6px;margin:0 auto;padding:12px 24px;background:none;border:none;cursor:pointer;color:#374151;font-size:13px">
<svg style="width:24px;height:24px;color:#64748b" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M6.827 6.175A2.31 2.31 0 015.186 7.23c-.38.054-.757.112-1.134.175C2.999 7.58 2.25 8.507 2.25 9.574V18a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9.574c0-1.067-.75-1.994-1.802-2.169a47.865 47.865 0 00-1.134-.175 2.31 2.31 0 01-1.64-1.055l-.822-1.316a2.192 2.192 0 00-1.736-1.039 48.774 48.774 0 00-5.232 0 2.192 2.192 0 00-1.736 1.039l-.821 1.316z"/><path stroke-linecap="round" stroke-linejoin="round" d="M16.5 12.75a4.5 4.5 0 11-9 0 4.5 4.5 0 019 0zM18.75 10.5h.008v.008h-.008V10.5z"/></svg>
<span>Take Photo or Upload ID</span>
</button>
<div id="dot-id-upload-options">
<div style="display:flex;flex-wrap:wrap;gap:8px;justify-content:center">
<button type="button" id="dot-id-btn" style="display:flex;align-items:center;gap:8px;padding:10px 20px;background:#fff;border:1px solid #d1d5db;border-radius:8px;cursor:pointer;color:#374151;font-size:13px;font-weight:500">
<svg style="width:20px;height:20px;color:#64748b" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5"/></svg>
Upload File
</button>
<button type="button" id="dot-id-scan-btn" style="display:flex;align-items:center;gap:8px;padding:10px 20px;background:#fff;border:1px solid #d1d5db;border-radius:8px;cursor:pointer;color:#374151;font-size:13px;font-weight:500">
<svg style="width:20px;height:20px;color:#64748b" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M6.827 6.175A2.31 2.31 0 015.186 7.23c-.38.054-.757.112-1.134.175C2.999 7.58 2.25 8.507 2.25 9.574V18a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9.574c0-1.067-.75-1.994-1.802-2.169a47.865 47.865 0 00-1.134-.175 2.31 2.31 0 01-1.64-1.055l-.822-1.316a2.192 2.192 0 00-1.736-1.039 48.774 48.774 0 00-5.232 0 2.192 2.192 0 00-1.736 1.039l-.821 1.316z"/><path stroke-linecap="round" stroke-linejoin="round" d="M16.5 12.75a4.5 4.5 0 11-9 0 4.5 4.5 0 019 0zM18.75 10.5h.008v.008h-.008V10.5z"/></svg>
Camera / Scanner
</button>
</div>
<input type="file" id="dot-photo-id-scan" accept="image/*" capture="environment" style="display:none" />
<div style="text-align:center;margin:12px 0 0">
<div style="display:flex;align-items:center;gap:12px;justify-content:center;margin-bottom:8px">
<div style="flex:1;height:1px;background:#d1d5db"></div>
<span style="font-size:11px;color:#94a3b8;white-space:nowrap">or use your phone</span>
<div style="flex:1;height:1px;background:#d1d5db"></div>
</div>
<button type="button" id="dot-id-qr-btn" style="display:inline-flex;align-items:center;gap:8px;padding:8px 16px;background:#f8fafc;border:1px solid #d1d5db;border-radius:6px;cursor:pointer;font-size:12px;color:#374151">
<svg style="width:16px;height:16px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 013.75 9.375v-4.5zM3.75 14.625c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM13.5 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0113.5 9.375v-4.5z"/><path stroke-linecap="round" stroke-linejoin="round" d="M6.75 6.75h.75v.75h-.75v-.75zM6.75 16.5h.75v.75h-.75v-.75zM16.5 6.75h.75v.75h-.75v-.75zM13.5 13.5h.75v.75h-.75v-.75zM13.5 19.5h.75v.75h-.75v-.75zM19.5 13.5h.75v.75h-.75v-.75zM19.5 19.5h.75v.75h-.75v-.75zM16.5 16.5h.75v.75h-.75v-.75z"/></svg>
Scan QR code to upload from phone
</button>
</div>
<div id="dot-id-qr-container" hidden style="text-align:center;margin:16px 0 0;padding:16px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px">
<canvas id="dot-id-qr-canvas" style="margin:0 auto;display:block"></canvas>
<p style="font-size:12px;color:#64748b;margin:10px 0 0">Scan this QR code with your phone's camera to open this form on your mobile device and take a photo of your ID.</p>
</div>
</div>
</div>
<div class="pw-security-notice" style="margin-top:8px">
<svg style="width:14px;height:14px;flex-shrink:0;margin-top:2px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 10-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H6.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z"/></svg>
@ -402,24 +426,74 @@
const idPreview = document.getElementById("dot-id-preview");
const idImg = document.getElementById("dot-id-img");
const idRemove = document.getElementById("dot-id-remove");
idBtn?.addEventListener("click", () => idInput?.click());
idInput?.addEventListener("change", () => {
const file = idInput.files?.[0];
const idQrBtn = document.getElementById("dot-id-qr-btn");
const idQrContainer = document.getElementById("dot-id-qr-container");
const idUploadOpts = document.getElementById("dot-id-upload-options");
// Upload button — opens file picker
idBtn?.addEventListener("click", function() { if (idInput) idInput.click(); });
// Scanner/camera button — uses capture attribute to trigger camera or scanner
var idScanBtn = document.getElementById("dot-id-scan-btn");
var idScanInput = document.getElementById("dot-photo-id-scan");
idScanBtn?.addEventListener("click", function() { if (idScanInput) idScanInput.click(); });
idScanInput?.addEventListener("change", function() {
var file = idScanInput.files?.[0];
if (!file) return;
(window).__dotPhotoId = file;
// Copy to main input handler
window.__dotPhotoId = file;
if (file.type.startsWith("image/") && idImg) {
var reader = new FileReader();
reader.onload = function(e) { idImg.src = e.target?.result; };
reader.readAsDataURL(file);
}
if (idPreview) idPreview.hidden = false;
if (idUploadOpts) idUploadOpts.style.display = "none";
});
// QR code button — show QR with current page URL for phone upload
idQrBtn?.addEventListener("click", function() {
if (!idQrContainer) return;
var showing = !idQrContainer.hidden;
idQrContainer.hidden = showing;
if (!showing) {
// Generate QR code using canvas
var canvas = document.getElementById("dot-id-qr-canvas");
if (canvas && !canvas.dataset.rendered) {
var url = window.location.href;
// Use a simple QR code API (Google Charts - no library needed)
var img = new Image();
img.crossOrigin = "anonymous";
img.onload = function() {
canvas.width = 200;
canvas.height = 200;
var ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0, 200, 200);
canvas.dataset.rendered = "1";
};
img.src = "https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=" + encodeURIComponent(url);
}
}
});
idInput?.addEventListener("change", function() {
var file = idInput.files?.[0];
if (!file) return;
window.__dotPhotoId = file;
if (file.type.startsWith("image/")) {
const reader = new FileReader();
reader.onload = (e) => { idImg.src = e.target?.result; };
reader.readAsDataURL(file);
}
if (idPreview) idPreview.hidden = false;
if (idBtn) idBtn.style.display = "none";
if (idUploadOpts) idUploadOpts.style.display = "none";
});
idRemove?.addEventListener("click", () => {
(window).__dotPhotoId = null;
if (idInput) idInput.value = "";
if (idScanInput) idScanInput.value = "";
if (idPreview) idPreview.hidden = true;
if (idBtn) idBtn.style.display = "flex";
if (idUploadOpts) idUploadOpts.style.display = "";
});
} // end guard