feat: Passwordless cross-device authentication

- Arkitektur: docs/auth/passwordless-architecture.md
- Backend: iom/quixzoom-auth-service/ (FastAPI + Redis)
- Webb: quixzoom-market-pages/se/login/ (QR-kod + polling)
- App: iom/quixzoom-app/src/features/auth/ (push + deep links)

Flöde: QR-kod → app-godkännande → webb-inloggad
This commit is contained in:
Bernt
2026-07-07 07:11:50 +00:00
parent 4aa984ad74
commit 6989a98d75
61843 changed files with 5491611 additions and 872231 deletions
+52 -52
View File
@@ -32,9 +32,9 @@
.nav-live {
font-size: 12px; font-weight: 600; padding: 4px 10px;
background: rgba(255,69,58,0.15); color: var(--ios-red);
border-radius: 20px; display: flex; align-items: center; gap: 5px;
border-radius: var(--radius-xl); display: flex; align-items: center; gap: 5px;
}
.live-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--ios-red); animation: pulse 1.5s infinite; }
.live-dot { width: 6px; height: 6px; border-radius: var(--radius-full); background: var(--ios-red); animation: pulse 1.5s infinite; }
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.4} }
/* Countdown */
@@ -48,7 +48,7 @@
text-align: center;
background: var(--background-secondary);
border: 0.5px solid var(--separator);
border-radius: 14px; padding: 14px 16px; min-width: 70px;
border-radius: var(--radius-lg); padding: 14px 16px; min-width: 70px;
}
.countdown-num {
font-family: var(--font-display);
@@ -68,7 +68,7 @@
.target-card {
flex: 1; background: var(--background-secondary);
border: 0.5px solid var(--separator);
border-radius: 14px; padding: 16px; text-align: center;
border-radius: var(--radius-lg); padding: 16px; text-align: center;
}
.target-icon { font-size: 24px; margin-bottom: 6px; }
.target-num { font-size: 26px; font-weight: 800; font-family: var(--font-display); letter-spacing: -0.5px; }
@@ -85,7 +85,7 @@
/* Checklist */
.checklist-card {
background: var(--background-secondary);
border-radius: 14px; overflow: hidden;
border-radius: var(--radius-lg); overflow: hidden;
border: 0.5px solid var(--separator);
}
.check-row {
@@ -97,7 +97,7 @@
.check-row:last-child { border-bottom: none; }
.check-row:active { opacity: 0.7; }
.check-box {
width: 24px; height: 24px; border-radius: 12px;
width: 24px; height: 24px; border-radius: var(--radius-md);
border: 2px solid var(--separator); flex-shrink: 0;
display: flex; align-items: center; justify-content: center;
font-size: 13px; transition: all 0.15s;
@@ -108,7 +108,7 @@
.check-title.done { text-decoration: line-through; color: var(--label-secondary); }
.check-sub { font-size: 12px; color: var(--label-secondary); margin-top: 2px; }
.check-tag {
font-size: 11px; font-weight: 600; padding: 3px 8px; border-radius: 6px;
font-size: 11px; font-weight: 600; padding: 3px 8px; border-radius: var(--radius-sm);
}
.tag-critical { background: rgba(255,69,58,0.15); color: var(--ios-red); }
.tag-content { background: rgba(10,132,255,0.12); color: var(--ios-blue); }
@@ -121,7 +121,7 @@
.channel-card {
background: var(--background-secondary);
border: 0.5px solid var(--separator);
border-radius: 14px; padding: 16px;
border-radius: var(--radius-lg); padding: 16px;
display: flex; gap: 14px; align-items: flex-start;
}
.channel-icon { font-size: 28px; flex-shrink: 0; }
@@ -130,7 +130,7 @@
.channel-desc { font-size: 13px; color: var(--label-secondary); line-height: 1.5; margin-bottom: 8px; }
.channel-action {
display: inline-flex; align-items: center; gap: 5px;
padding: 7px 12px; border-radius: 8px;
padding: 7px 12px; border-radius: var(--radius-md);
font-size: 13px; font-weight: 600;
background: var(--fill-secondary); color: var(--label-primary);
border: none; cursor: pointer; transition: opacity 0.12s;
@@ -141,7 +141,7 @@
/* Content Calendar */
.calendar-card {
background: var(--background-secondary);
border-radius: 14px; overflow: hidden;
border-radius: var(--radius-lg); overflow: hidden;
border: 0.5px solid var(--separator);
}
.calendar-week {
@@ -153,7 +153,7 @@
padding: 14px 16px 10px;
}
.week-badge {
font-size: 12px; font-weight: 700; padding: 3px 10px; border-radius: 8px;
font-size: 12px; font-weight: 700; padding: 3px 10px; border-radius: var(--radius-md);
}
.week-1 { background: rgba(10,132,255,0.15); color: var(--ios-blue); }
.week-2 { background: rgba(175,82,222,0.15); color: var(--ios-purple); }
@@ -164,7 +164,7 @@
.calendar-events { padding: 0 16px 14px; display: flex; flex-direction: column; gap: 8px; }
.cal-event {
display: flex; align-items: center; gap: 10px;
background: var(--fill-quaternary); border-radius: 10px; padding: 10px 12px;
background: var(--fill-quaternary); border-radius: var(--radius-md); padding: 10px 12px;
}
.cal-date { font-size: 13px; font-weight: 600; color: var(--label-secondary); min-width: 40px; }
.cal-text { font-size: 14px; flex: 1; }
@@ -175,7 +175,7 @@
margin: 0 16px 24px;
background: linear-gradient(135deg, rgba(255,69,58,0.12), rgba(255,159,10,0.08));
border: 0.5px solid rgba(255,69,58,0.25);
border-radius: 16px; padding: 20px;
border-radius: var(--radius-lg); padding: 20px;
text-align: center;
}
.launch-day-icon { font-size: 48px; margin-bottom: 10px; }
@@ -190,14 +190,14 @@
.press-card {
background: var(--background-secondary);
border: 0.5px solid var(--separator);
border-radius: 14px; padding: 16px;
border-radius: var(--radius-lg); padding: 16px;
margin: 0 16px 24px;
}
.press-title { font-size: 17px; font-weight: 600; margin-bottom: 8px; }
.press-body { font-size: 14px; color: var(--label-secondary); line-height: 1.6; margin-bottom: 12px; }
.press-btn {
display: inline-flex; align-items: center; gap: 6px;
padding: 10px 16px; border-radius: 10px;
padding: 10px 16px; border-radius: var(--radius-md);
background: var(--ios-blue); color: #fff;
font-size: 14px; font-weight: 600;
border: none; cursor: pointer;
@@ -254,7 +254,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<!-- Countdown -->
<div class="countdown-hero">
<div class="countdown-title">🚀 Countdown till lansering</div>
<div class="countdown-title"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2L10 8L14 9L8 14L6 8L2 7L8 2Z" fill="#666"/></svg> Countdown till lansering</div>
<div class="countdown-grid">
<div class="countdown-unit">
<div class="countdown-num" id="cd-days"></div>
@@ -279,17 +279,17 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<!-- Targets -->
<div class="target-row">
<div class="target-card">
<div class="target-icon">🎯</div>
<div class="target-icon"></div>
<div class="target-num" style="color:var(--ios-blue)">100</div>
<div class="target-label">Zoomers dag 1</div>
</div>
<div class="target-card">
<div class="target-icon">📦</div>
<div class="target-icon"></div>
<div class="target-num" style="color:var(--ios-green)">1 000</div>
<div class="target-label">Uppdrag månad 1</div>
</div>
<div class="target-card">
<div class="target-icon">💰</div>
<div class="target-icon"></div>
<div class="target-num" style="color:var(--ios-orange)">500k</div>
<div class="target-label">GMV månad 1 kr</div>
</div>
@@ -307,38 +307,38 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="channels-grid">
<div class="channel-card">
<div class="channel-icon">💼</div>
<div class="channel-icon"></div>
<div class="channel-info">
<div class="channel-name">LinkedIn</div>
<div class="channel-desc">Företagspost från Wavult Group + personlig post från Erik. Target: fastighetsägare, byggföretag, infrastrukturbolag.</div>
<a class="channel-action" href="https://linkedin.com/company/wavult" target="_blank">📝 Öppna LinkedIn</a>
<a class="channel-action" href="https://linkedin.com/company/wavult" target="_blank"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M3 13V10L10 3L13 6L6 13H3Z" stroke="#666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M9 4L12 7" stroke="#666" stroke-width="1.5"/></svg> Öppna LinkedIn</a>
</div>
</div>
<div class="channel-card">
<div class="channel-icon">𝕏</div>
<div class="channel-icon"></div>
<div class="channel-info">
<div class="channel-name">Twitter / X Thread</div>
<div class="channel-desc">Lanserings-thread: "Vi bygger Uber för drönare i Sverige 🇸🇪" — 8 tweets med bilder, stats och CTA till App Store.</div>
<button class="channel-action" onclick="copyTweet()">📋 Kopiera launch tweet</button>
<div class="channel-desc">Lanserings-thread: "Vi bygger Uber för drönare i Sverige <span class="flag-se">SE</span>" — 8 tweets med bilder, stats och CTA till App Store.</div>
<button class="channel-action" onclick="copyTweet()"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="3" y="2" width="10" height="12" rx="1" stroke="#666" stroke-width="1.5"/><path d="M6 5H10M6 8H10M6 11H8" stroke="#666" stroke-width="1.5" stroke-linecap="round"/></svg> Kopiera launch tweet</button>
</div>
</div>
<div class="channel-card">
<div class="channel-icon">🚀</div>
<div class="channel-icon"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2L10 8L14 9L8 14L6 8L2 7L8 2Z" fill="#666"/></svg></div>
<div class="channel-info">
<div class="channel-name">Product Hunt</div>
<div class="channel-desc">Submit dag 1 (1 augusti). Mål: Top 5 Product of the Day. Mobilisera team för upvotes på morgonen.</div>
<a class="channel-action" href="https://producthunt.com/posts/new" target="_blank">🔗 Submit to PH</a>
<a class="channel-action" href="https://producthunt.com/posts/new" target="_blank"> Submit to PH</a>
</div>
</div>
<div class="channel-card">
<div class="channel-icon">📰</div>
<div class="channel-icon"></div>
<div class="channel-info">
<div class="channel-name">Press Release</div>
<div class="channel-desc">Skicka till Breakit, Ny Teknik, DI Digital. Embargo lift: 1 augusti 08:00. Distribution: TT Nyhetsbyrån + direkt kontakt.</div>
<button class="channel-action" onclick="showPR()">📄 Visa pressrelease</button>
<button class="channel-action" onclick="showPR()"> Visa pressrelease</button>
</div>
</div>
@@ -347,7 +347,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<!-- LAUNCH DAY Card -->
<div class="launch-day-card">
<div class="launch-day-icon">🎯</div>
<div class="launch-day-icon"></div>
<div class="launch-day-title">LAUNCH DAY — 1 augusti</div>
<div class="launch-day-sub">08:00 — App Store live<br>08:30 — LinkedIn + Twitter posts<br>09:00 — Product Hunt live<br>10:00 — Press release distribution<br>12:00 — Status check: zoomers onboardade<br>18:00 — End of day report</div>
</div>
@@ -368,23 +368,23 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="calendar-events">
<div class="cal-event">
<span class="cal-date">Jun 1</span>
<span class="cal-text">LinkedIn: "Något är på väg i Sverige... 🇸🇪🚁"</span>
<span class="cal-icon">💼</span>
<span class="cal-text">LinkedIn: "Något är på väg i Sverige... <span class="flag-se">SE</span><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2V6M4 4H12" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M2 8H6L8 6L10 8H14" stroke="#666" stroke-width="1.5" stroke-linejoin="round"/><path d="M6 8V12H10V8" stroke="#666" stroke-width="1.5"/></svg>"</span>
<span class="cal-icon"></span>
</div>
<div class="cal-event">
<span class="cal-date">Jun 3</span>
<span class="cal-text">Twitter: Bakom kulisserna — drönare i aktion</span>
<span class="cal-icon">𝕏</span>
<span class="cal-icon"></span>
</div>
<div class="cal-event">
<span class="cal-date">Jun 5</span>
<span class="cal-text">Waitlist open: "Be first Zoomer in Sweden"</span>
<span class="cal-icon">🎯</span>
<span class="cal-icon"></span>
</div>
<div class="cal-event">
<span class="cal-date">Jun 7</span>
<span class="cal-text">Instagram Reel: Drönarvideo från pilot-uppdrag</span>
<span class="cal-icon">📱</span>
<span class="cal-icon"></span>
</div>
</div>
</div>
@@ -400,18 +400,18 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="calendar-events">
<div class="cal-event">
<span class="cal-date">Jun 8</span>
<span class="cal-text">✈️ Inlämning App Store (deadline)</span>
<span class="cal-icon">📱</span>
<span class="cal-text"> Inlämning App Store (deadline)</span>
<span class="cal-icon"></span>
</div>
<div class="cal-event">
<span class="cal-date">Jun 9</span>
<span class="cal-text">Beta zoomer-berättelse #1: "Mitt första uppdrag"</span>
<span class="cal-icon">👤</span>
<span class="cal-icon"></span>
</div>
<div class="cal-event">
<span class="cal-date">Jun 11</span>
<span class="cal-text">LinkedIn: "Varför vi bygger quiXzoom" — långpost</span>
<span class="cal-icon">💼</span>
<span class="cal-icon"></span>
</div>
<div class="cal-event">
<span class="cal-date">Jun 13</span>
@@ -421,7 +421,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="cal-event">
<span class="cal-date">Jun 14</span>
<span class="cal-text">Press kit skickat · Media briefing</span>
<span class="cal-icon">📰</span>
<span class="cal-icon"></span>
</div>
</div>
</div>
@@ -429,7 +429,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="calendar-week">
<div class="calendar-week-header">
<div>
<span class="week-badge week-launch">🚀 LAUNCH DAY</span>
<span class="week-badge week-launch"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2L10 8L14 9L8 14L6 8L2 7L8 2Z" fill="#666"/></svg> LAUNCH DAY</span>
<div class="week-title" style="margin-top:4px">1 augusti 2026</div>
<div class="week-sub">All hands on deck</div>
</div>
@@ -437,23 +437,23 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="calendar-events">
<div class="cal-event">
<span class="cal-date">08:00</span>
<span class="cal-text">App Store LIVE 🎉</span>
<span class="cal-icon">🍎</span>
<span class="cal-text">App Store LIVE </span>
<span class="cal-icon"></span>
</div>
<div class="cal-event">
<span class="cal-date">08:30</span>
<span class="cal-text">LinkedIn + Twitter launch posts</span>
<span class="cal-icon">📣</span>
<span class="cal-icon"></span>
</div>
<div class="cal-event">
<span class="cal-date">09:00</span>
<span class="cal-text">Product Hunt submission</span>
<span class="cal-icon">🚀</span>
<span class="cal-icon"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2L10 8L14 9L8 14L6 8L2 7L8 2Z" fill="#666"/></svg></span>
</div>
<div class="cal-event">
<span class="cal-date">10:00</span>
<span class="cal-text">Press release distribution (TT, Breakit)</span>
<span class="cal-icon">📰</span>
<span class="cal-icon"></span>
</div>
</div>
</div>
@@ -465,10 +465,10 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<!-- Press Release Modal -->
<div id="pr-modal" style="display:none;position:fixed;inset:0;z-index:9999;background: rgba(242,242,247,0.8);backdrop-filter:blur(8px);overflow-y:auto;padding:20px">
<div style="background:var(--background-secondary);border-radius:20px;max-width:600px;margin:0 auto;padding:24px;border:0.5px solid var(--separator)">
<div style="background:var(--background-secondary);border-radius: var(--radius-xl);max-width:600px;margin:0 auto;padding:24px;border:0.5px solid var(--separator)">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
<div style="font-size:18px;font-weight:700">📰 Pressrelease</div>
<button onclick="hidePR()" style="background:var(--fill-secondary);border:none;border-radius:50%;width:32px;height:32px;font-size:18px;cursor:pointer;color:var(--label-primary)">×</button>
<div style="font-size:18px;font-weight:700"> Pressrelease</div>
<button onclick="hidePR()" style="background:var(--fill-secondary);border:none;border-radius: var(--radius-full);width:32px;height:32px;font-size:18px;cursor:pointer;color:var(--label-primary)">×</button>
</div>
<div style="font-size:11px;color:var(--label-secondary);margin-bottom:16px;text-transform:uppercase;letter-spacing:0.5px">EMBARGO: 1 augusti 2026 · 08:00 CET</div>
<div style="font-size:18px;font-weight:700;margin-bottom:12px;line-height:1.4">quiXzoom lanserar i Sverige — den första drönargig-plattformen för den svenska marknaden</div>
@@ -478,7 +478,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
Plattformen finns tillgänglig från idag i hela Sverige via App Store. Zoomer-registrering är öppen för certifierade drönarpilöter.
</div>
<div style="font-size:12px;color:var(--label-tertiary);margin-bottom:16px">Kontakt: erik@hypbit.com · +46709123223</div>
<button onclick="copyPR()" style="background:var(--ios-blue);color:#fff;border:none;border-radius:10px;padding:12px 20px;font-size:14px;font-weight:600;cursor:pointer;width:100%">📋 Kopiera pressrelease</button>
<button onclick="copyPR()" style="background:var(--ios-blue);color:#fff;border:none;border-radius: var(--radius-md);padding:12px 20px;font-size:14px;font-weight:600;cursor:pointer;width:100%"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="3" y="2" width="10" height="12" rx="1" stroke="#666" stroke-width="1.5"/><path d="M6 5H10M6 8H10M6 11H8" stroke="#666" stroke-width="1.5" stroke-linecap="round"/></svg> Kopiera pressrelease</button>
</div>
</div>
@@ -536,7 +536,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
checks.forEach((c, i) => { if (savedChecks[i]) c.done = true; });
function tagLabel(t) {
return { critical:'🔴 Kritisk', content:'🔵 Content', tech:'🟣 Tech', marketing:'🟠 Marketing', done:' Klar' }[t] || t;
return { critical:' Kritisk', content:' Content', tech:'🟣 Tech', marketing:'🟠 Marketing', done:'<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Klar' }[t] || t;
}
function tagClass(t) {
return { critical:'tag-critical', content:'tag-content', tech:'tag-tech', marketing:'tag-marketing', done:'tag-done' }[t] || '';
@@ -549,7 +549,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
el.innerHTML = checks.map((c,i) => `
<div class="check-row" onclick="toggleCheck(${i})">
<div class="check-box ${c.done?'done':''}">${c.done?'':''}</div>
<div class="check-box ${c.done?'done':''}">${c.done?'<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M3 8L6.5 11.5L13 4.5" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>':''}</div>
<div class="check-info">
<div class="check-title ${c.done?'done':''}">${c.text}</div>
<div class="check-sub">${c.sub}</div>
@@ -571,7 +571,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
// Tweet
function copyTweet() {
const tweet = `Vi lanserar quiXzoom idag 🚁🇸🇪\n\nSveriges första drönargig-plattform.\n\nZoomers = certifierade drönarpilöter som tar uppdrag när det passar dem.\n\nFasad-scanning, takinspektion, infrastruktur — på 24h.\n\nLadda ner nu 👇\nhttps://quixzoom.com/app`;
const tweet = `Vi lanserar quiXzoom idag <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2V6M4 4H12" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M2 8H6L8 6L10 8H14" stroke="#666" stroke-width="1.5" stroke-linejoin="round"/><path d="M6 8V12H10V8" stroke="#666" stroke-width="1.5"/></svg><span class="flag-se">SE</span>\n\nSveriges första drönargig-plattform.\n\nZoomers = certifierade drönarpilöter som tar uppdrag när det passar dem.\n\nFasad-scanning, takinspektion, infrastruktur — på 24h.\n\nLadda ner nu \nhttps://quixzoom.com/app`;
navigator.clipboard?.writeText(tweet).then(() => alert('Tweet kopierad!')).catch(() => alert(tweet));
}