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
+71 -71
View File
@@ -31,7 +31,7 @@
line-height: 1.6;
}
/* ── Nav ──────────────────────────────────────────────────── */
/* Nav */
nav {
background: var(--navy);
padding: 0 2rem;
@@ -63,7 +63,7 @@
color: var(--white);
border: none;
padding: 0.5rem 1.25rem;
border-radius: 6px;
border-radius: var(--radius-sm);
font-weight: 600;
font-size: 0.9rem;
cursor: pointer;
@@ -72,7 +72,7 @@
}
.nav-cta:hover { background: var(--amber2); }
/* ── Hero ─────────────────────────────────────────────────── */
/* Hero */
.hero {
background: var(--navy);
color: var(--white);
@@ -106,7 +106,7 @@
.launch-badge .dot {
width: 7px; height: 7px;
background: var(--amber);
border-radius: 50%;
border-radius: var(--radius-full);
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
@@ -138,7 +138,7 @@
color: var(--white);
border: none;
padding: 0.85rem 2rem;
border-radius: 8px;
border-radius: var(--radius-md);
font-size: 1rem;
font-weight: 700;
cursor: pointer;
@@ -151,7 +151,7 @@
color: var(--white);
border: 2px solid rgba(255,255,255,0.4);
padding: 0.85rem 2rem;
border-radius: 8px;
border-radius: var(--radius-md);
font-size: 1rem;
font-weight: 600;
cursor: pointer;
@@ -160,7 +160,7 @@
}
.btn-outline:hover { border-color: rgba(255,255,255,0.8); }
/* ── Countdown ────────────────────────────────────────────── */
/* Countdown */
.countdown-section {
background: var(--navy);
padding-bottom: 4rem;
@@ -199,7 +199,7 @@
margin-top: 0.3rem;
}
/* ── Section base ─────────────────────────────────────────── */
/* Section base */
section { padding: 5rem 2rem; }
.container { max-width: 1100px; margin: 0 auto; }
.section-tag {
@@ -225,7 +225,7 @@
margin-bottom: 3rem;
}
/* ── Stats strip ──────────────────────────────────────────── */
/* Stats strip */
.stats-strip {
background: var(--white);
border-top: 1px solid var(--border);
@@ -248,7 +248,7 @@
}
.stat-label { font-size: 0.85rem; color: var(--muted); margin-top: 0.25rem; }
/* ── How it works ─────────────────────────────────────────── */
/* How it works */
.steps-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
@@ -264,7 +264,7 @@
width: 36px; height: 36px;
background: var(--amber);
color: var(--white);
border-radius: 50%;
border-radius: var(--radius-full);
display: flex;
align-items: center;
justify-content: center;
@@ -275,7 +275,7 @@
.step-card h3 { font-size: 1rem; font-weight: 700; margin-bottom: 0.5rem; color: var(--navy); }
.step-card p { font-size: 0.9rem; color: var(--muted); line-height: 1.5; }
/* ── Cities ───────────────────────────────────────────────── */
/* Cities */
.cities-section { background: var(--navy); color: var(--white); }
.cities-section h2 { color: var(--white); }
.cities-section .lead { color: rgba(255,255,255,0.75); }
@@ -306,13 +306,13 @@
font-size: 0.68rem;
font-weight: 700;
padding: 0.15rem 0.5rem;
border-radius: 4px;
border-radius: var(--radius-sm);
margin-top: 0.5rem;
text-transform: uppercase;
letter-spacing: 0.06em;
}
/* ── Features ─────────────────────────────────────────────── */
/* Features */
.features-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
@@ -327,7 +327,7 @@
.feature-icon {
width: 44px; height: 44px;
background: rgba(200,118,58,0.1);
border-radius: 10px;
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
@@ -346,11 +346,11 @@
font-size: 0.72rem;
font-weight: 600;
padding: 0.2rem 0.6rem;
border-radius: 4px;
border-radius: var(--radius-sm);
margin-top: 0.75rem;
}
/* ── DAC7 + Skatt ─────────────────────────────────────────── */
/* DAC7 + Skatt */
.compliance-section { background: #F4F2EE; }
.compliance-grid {
display: grid;
@@ -379,7 +379,7 @@
.compliance-list li:last-child { border-bottom: none; }
.compliance-list .check { color: var(--green); font-weight: 700; flex-shrink: 0; }
/* ── Earnings calculator ──────────────────────────────────── */
/* Earnings calculator */
.calculator-section { background: var(--white); }
.calc-box {
background: var(--cream);
@@ -400,7 +400,7 @@
.calc-result {
background: var(--navy);
color: var(--white);
border-radius: 8px;
border-radius: var(--radius-md);
padding: 1.25rem 1.5rem;
margin-top: 1.5rem;
display: flex;
@@ -412,7 +412,7 @@
/* ── Registration / CTA ───────────────────────────────────── */
/* Registration / CTA */
.cta-section {
background: var(--navy);
color: var(--white);
@@ -434,7 +434,7 @@
min-width: 200px;
padding: 0.85rem 1.1rem;
border: 1.5px solid rgba(255,255,255,0.2);
border-radius: 8px;
border-radius: var(--radius-md);
background: rgba(255,255,255,0.08);
color: var(--white);
font-size: 0.95rem;
@@ -454,14 +454,14 @@
background: rgba(45,122,79,0.2);
border: 1px solid rgba(45,122,79,0.5);
color: #6ECFA0;
border-radius: 8px;
border-radius: var(--radius-md);
padding: 1rem 1.5rem;
max-width: 480px;
margin: 1rem auto 0;
font-size: 0.95rem;
}
/* ── Footer ───────────────────────────────────────────────── */
/* Footer */
footer {
background: #000D22;
color: rgba(255,255,255,0.75);
@@ -480,7 +480,7 @@
}
.footer-logo span { color: var(--amber); }
/* ── Responsive ───────────────────────────────────────────── */
/* Responsive */
@media (max-width: 640px) {
.nav-links { display: none; }
.hero { padding: 3rem 1.5rem 3rem; }
@@ -514,7 +514,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</head>
<body>
<!-- ── Nav ──────────────────────────────────────────────────────────────── -->
<!-- Nav -->
<nav>
<div class="nav-logo">qui<span>X</span>zoom</div>
<div class="nav-links">
@@ -526,7 +526,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<a href="#register" class="nav-cta">Bli Zoomer</a>
</nav>
<!-- ── Hero ─────────────────────────────────────────────────────────────── -->
<!-- Hero -->
<header class="hero">
<div class="launch-badge">
<span class="dot"></span>
@@ -542,7 +542,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
</header>
<!-- ── Countdown ─────────────────────────────────────────────────────────── -->
<!-- Countdown -->
<div class="countdown-section">
<div class="countdown-container" id="countdown">
<div class="countdown-unit">
@@ -567,7 +567,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
</div>
<!-- ── Stats strip ────────────────────────────────────────────────────────── -->
<!-- Stats strip -->
<div class="stats-strip">
<div class="stats-grid">
<div>
@@ -593,7 +593,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
</div>
<!-- ── How it works ───────────────────────────────────────────────────────── -->
<!-- How it works -->
<section id="how-it-works" style="background: var(--cream);">
<div class="container">
<div class="section-tag">Hur det fungerar</div>
@@ -624,7 +624,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
</section>
<!-- ── Cities ─────────────────────────────────────────────────────────────── -->
<!-- Cities -->
<section id="cities" class="cities-section">
<div class="container">
<div class="section-tag" style="color: rgba(200,118,58,0.9);">Sverige-launch</div>
@@ -632,40 +632,40 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<p class="lead">quiXzoom öppnar i Sveriges fem största städer den 1 augusti 2026 — med fler städer planerade under H2.</p>
<div class="cities-grid">
<div class="city-card">
<div class="city-icon">🏙️</div>
<div class="city-icon"></div>
<div class="city-name">Stockholm</div>
<div class="city-missions">89 aktiva zoomers • 312 uppdrag</div>
<div class="city-live-badge"> Live 15 jun</div>
<div class="city-live-badge"><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> Live 15 jun</div>
</div>
<div class="city-card">
<div class="city-icon"></div>
<div class="city-icon"></div>
<div class="city-name">Göteborg</div>
<div class="city-missions">41 aktiva zoomers • 148 uppdrag</div>
<div class="city-live-badge"> Live 15 jun</div>
<div class="city-live-badge"><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> Live 15 jun</div>
</div>
<div class="city-card">
<div class="city-icon">🌉</div>
<div class="city-icon"></div>
<div class="city-name">Malmö</div>
<div class="city-missions">28 aktiva zoomers • 97 uppdrag</div>
<div class="city-live-badge"> Live 15 jun</div>
<div class="city-live-badge"><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> Live 15 jun</div>
</div>
<div class="city-card">
<div class="city-icon">🎓</div>
<div class="city-icon"></div>
<div class="city-name">Uppsala</div>
<div class="city-missions">15 aktiva zoomers • 54 uppdrag</div>
<div class="city-live-badge"> Live 15 jun</div>
<div class="city-live-badge"><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> Live 15 jun</div>
</div>
<div class="city-card">
<div class="city-icon">⚙️</div>
<div class="city-icon"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="3" stroke="#666" stroke-width="1.5"/><path d="M8 2V4M8 12V14M2 8H4M12 8H14M4.3 4.3L5.7 5.7M10.3 10.3L11.7 11.7M4.3 11.7L5.7 10.3M10.3 5.7L11.7 4.3" stroke="#666" stroke-width="1.5" stroke-linecap="round"/></svg></div>
<div class="city-name">Västerås</div>
<div class="city-missions">9 aktiva zoomers • 31 uppdrag</div>
<div class="city-live-badge"> Live 15 jun</div>
<div class="city-live-badge"><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> Live 15 jun</div>
</div>
</div>
</div>
</section>
<!-- ── Features ───────────────────────────────────────────────────────────── -->
<!-- Features -->
<section style="background: var(--cream);">
<div class="container">
<div class="section-tag">Plattformen</div>
@@ -673,46 +673,46 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<p class="lead">Allt du behöver för att driva din fotografi- eller videoverksamhet via quiXzoom — utan mellanhänder.</p>
<div class="features-grid">
<div class="feature-card">
<div class="feature-icon">💸</div>
<div class="feature-icon"></div>
<h3>Stripe Connect-utbetalning</h3>
<p>Pengarna är hos dig inom minuter efter att kunden godkänt leveransen. Inga banköverföringar som tar dagar.</p>
<span class="feature-badge"> Direktbetalning i SEK</span>
<span class="feature-badge"><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> Direktbetalning i SEK</span>
</div>
<div class="feature-card">
<div class="feature-icon">🗺️</div>
<div class="feature-icon"></div>
<h3>Uppdrag på kartan</h3>
<p>Realtidskarta visar öppna uppdrag i din stad. Filtrera på typ, ersättning och tidpunkt.</p>
<span class="feature-badge"> Livekarta med WebSocket</span>
<span class="feature-badge"><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> Livekarta med WebSocket</span>
</div>
<div class="feature-card">
<div class="feature-icon">🤖</div>
<div class="feature-icon"></div>
<h3>AI-matchning</h3>
<p>Vi matchar dig med uppdrag baserat på din utrustning, rating och plats — maximala möjligheter, minimalt letande.</p>
<span class="feature-badge"> Automatisk matching</span>
<span class="feature-badge"><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> Automatisk matching</span>
</div>
<div class="feature-card">
<div class="feature-icon">🆔</div>
<div class="feature-icon"></div>
<h3>Personnummer-KYC</h3>
<p>Säker identitetsverifiering via Skatteverket SPAR. Krävs för alla svenska zoomers — skyddar både dig och kunden.</p>
<span class="feature-badge"> SPAR-integrerat</span>
<span class="feature-badge"><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> SPAR-integrerat</span>
</div>
<div class="feature-card">
<div class="feature-icon">📊</div>
<div class="feature-icon"></div>
<h3>Skatteunderlag & DAC7</h3>
<p>quiXzoom sköter automatisk DAC7-rapportering till Skatteverket. Du får ett klart årsunderlag för din deklaration.</p>
<span class="feature-badge"> EU Direktiv 2021/514</span>
<span class="feature-badge"><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> EU Direktiv 2021/514</span>
</div>
<div class="feature-card">
<div class="feature-icon"></div>
<div class="feature-icon"></div>
<h3>Rating & badges</h3>
<p>Bygg ditt rykte med omdömen och badges — Topp-zoomer, HD-expert, Drönarpilot. Högre rating = fler uppdrag.</p>
<span class="feature-badge"> Viktad 30-dagarsrating</span>
<span class="feature-badge"><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> Viktad 30-dagarsrating</span>
</div>
</div>
</div>
</section>
<!-- ── Earnings calculator ────────────────────────────────────────────────── -->
<!-- Earnings calculator -->
<section id="earnings" class="calculator-section">
<div class="container">
<div class="section-tag">Förtjänst</div>
@@ -747,7 +747,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
</section>
<!-- ── DAC7 & Compliance ───────────────────────────────────────────────────── -->
<!-- DAC7 & Compliance -->
<section id="compliance" class="compliance-section">
<div class="container">
<div class="section-tag">Skatt & Efterlevnad</div>
@@ -757,28 +757,28 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="compliance-box">
<h3>DAC7 — EU Direktiv 2021/514</h3>
<ul class="compliance-list">
<li><span class="check"></span> Automatisk rapportering av zoomer-inkomster till Skatteverket</li>
<li><span class="check"></span> Rapporteringströskel: >30 uppdrag ELLER >2 000 EUR per år</li>
<li><span class="check"></span> Personnummer-verifiering via Skatteverket SPAR</li>
<li><span class="check"></span> Årsrapport skickas senast 31 januari varje år</li>
<li><span class="check"></span> Du får en kopia som skatteunderlag</li>
<li><span class="check"><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></span> Automatisk rapportering av zoomer-inkomster till Skatteverket</li>
<li><span class="check"><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></span> Rapporteringströskel: >30 uppdrag ELLER >2 000 EUR per år</li>
<li><span class="check"><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></span> Personnummer-verifiering via Skatteverket SPAR</li>
<li><span class="check"><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></span> Årsrapport skickas senast 31 januari varje år</li>
<li><span class="check"><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></span> Du får en kopia som skatteunderlag</li>
</ul>
</div>
<div class="compliance-box">
<h3>Stripe Connect-utbetalningar</h3>
<ul class="compliance-list">
<li><span class="check"></span> Stripe Connect — säker utbetalning</li>
<li><span class="check"></span> Kräver svenska mobilnummer (07X XXXXXXXX)</li>
<li><span class="check"></span> Utbetalning i SEK — inga valutaomräkningar</li>
<li><span class="check"></span> Minimibelopp: 100 SEK per utbetalning</li>
<li><span class="check"></span> Transaktionslogg tillgänglig i appen</li>
<li><span class="check"><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></span> Stripe Connect — säker utbetalning</li>
<li><span class="check"><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></span> Kräver svenska mobilnummer (07X XXXXXXXX)</li>
<li><span class="check"><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></span> Utbetalning i SEK — inga valutaomräkningar</li>
<li><span class="check"><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></span> Minimibelopp: 100 SEK per utbetalning</li>
<li><span class="check"><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></span> Transaktionslogg tillgänglig i appen</li>
</ul>
</div>
</div>
</div>
</section>
<!-- ── Registration CTA ───────────────────────────────────────────────────── -->
<!-- Registration CTA -->
<section id="register" class="cta-section">
<div class="container">
<div class="section-tag" style="color:rgba(200,118,58,0.9)">Sverige-launch 1 augusti 2026</div>
@@ -793,12 +793,12 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<strong id="waitlist-display">482</strong> zoomers i kö — bli nummer <strong id="waitlist-next">483</strong>
</div>
<div class="success-msg" id="success-msg">
🎉 Välkommen i kön! Vi hör av oss senast 1 augusti med dina inloggningsuppgifter och instruktioner för KYC.
Välkommen i kön! Vi hör av oss senast 1 augusti med dina inloggningsuppgifter och instruktioner för KYC.
</div>
</div>
</section>
<!-- ── Footer ─────────────────────────────────────────────────────────────── -->
<!-- Footer -->
<footer>
<div class="footer-logo">qui<span>X</span>zoom Sverige</div>
<div style="margin-bottom:0.75rem">
@@ -812,7 +812,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</footer>
<script>
// ── Countdown till 1 augusti 2026 ───────────────────────────────────────────
// Countdown till 1 augusti 2026
const LAUNCH = new Date('2026-06-15T08:00:00+02:00').getTime();
function updateCountdown() {
@@ -830,7 +830,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
updateCountdown();
setInterval(updateCountdown, 1000);
// ── Earnings calculator ───────────────────────────────────────────────────
// Earnings calculator
function updateCalc() {
const missions = parseInt(document.getElementById('missions-slider').value, 10);
const rate = parseInt(document.getElementById('rate-slider').value, 10);
@@ -844,7 +844,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
}
updateCalc();
// ── Waitlist ──────────────────────────────────────────────────────────────
// Waitlist
let waitlistCount = 482;
async function loadWaitlist() {