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
@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Zoomer Onboarding — quiXzoom</title>
<style>
/* ── iOS 26 / Apple Native — Light Mode ──────────────────────────────── */
/* iOS 26 / Apple Native — Light Mode */
:root {
--bg: #f2f2f7;
--bg-card: #ffffff;
@@ -42,7 +42,7 @@
overflow-x: hidden;
}
/* ── Screen wrapper ───────────────────────────────────────────────────── */
/* Screen wrapper */
.screen {
display: none;
min-height: 100dvh;
@@ -62,7 +62,7 @@
.screen.slide-in-right { animation: slideInRight 0.32s cubic-bezier(0.32,0.72,0,1) forwards; }
.screen.slide-in-left { animation: slideInLeft 0.32s cubic-bezier(0.32,0.72,0,1) forwards; }
/* ── Nav Bar ─────────────────────────────────────────────────────────── */
/* Nav Bar */
.nav-bar {
position: sticky;
top: 0;
@@ -101,7 +101,7 @@
}
.nav-spacer { width: 60px; }
/* ── Progress Indicator ──────────────────────────────────────────────── */
/* Progress Indicator */
.progress-steps {
display: flex;
justify-content: center;
@@ -110,7 +110,7 @@
}
.step-dot {
height: 4px;
border-radius: 2px;
border-radius: var(--radius-sm);
background: var(--fill);
transition: all 0.3s ease;
}
@@ -118,16 +118,25 @@
.step-dot.done { background: var(--green); width: 12px; }
.step-dot.pending { width: 8px; }
/* ── Hero Area ────────────────────────────────────────────────────────── */
/* Hero Area */
.hero {
padding: 32px 28px 20px;
text-align: center;
}
.hero-emoji {
font-size: 64px;
line-height: 1;
margin-bottom: 16px;
display: block;
.hero-icon {
width: 64px;
height: 64px;
margin: 0 auto 16px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-lg);
background: linear-gradient(135deg, var(--tint), #5AC8FA);
color: #fff;
}
.hero-icon svg {
width: 32px;
height: 32px;
}
.hero-title {
font-size: 28px;
@@ -143,14 +152,14 @@
line-height: 1.5;
}
/* ── Content Area ────────────────────────────────────────────────────── */
/* Content Area */
.content {
flex: 1;
padding: 0 16px;
overflow-y: auto;
}
/* ── Card ─────────────────────────────────────────────────────────────── */
/* Card */
.card {
background: var(--bg-card);
border-radius: var(--radius-lg);
@@ -159,7 +168,7 @@
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
}
/* ── Form fields ─────────────────────────────────────────────────────── */
/* Form fields */
.field-row {
display: flex;
flex-direction: column;
@@ -189,7 +198,7 @@
.field-value::placeholder { color: var(--label-tertiary); }
.field-value::-webkit-date-and-time-value { text-align: left; }
/* ── Toggle row ──────────────────────────────────────────────────────── */
/* Toggle row */
.toggle-row {
display: flex;
align-items: center;
@@ -205,7 +214,7 @@
width: 51px;
height: 31px;
background: #e5e5ea;
border-radius: 16px;
border-radius: var(--radius-lg);
position: relative;
cursor: pointer;
transition: background 0.25s;
@@ -221,14 +230,14 @@
position: absolute;
top: 2px; left: 2px;
width: 27px; height: 27px;
border-radius: 50%;
border-radius: var(--radius-full);
background: #fff;
box-shadow: 0 2px 4px rgba(0,0,0,0.22);
transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.ios-toggle.on::after { transform: translateX(20px); }
/* ── Info row ────────────────────────────────────────────────────────── */
/* Info row */
.info-row {
display: flex;
align-items: center;
@@ -239,7 +248,7 @@
.info-row:last-child { border-bottom: none; }
.info-icon {
width: 36px; height: 36px;
border-radius: 10px;
border-radius: var(--radius-md);
display: flex; align-items: center; justify-content: center;
font-size: 18px;
flex-shrink: 0;
@@ -249,7 +258,7 @@
.info-sub { font-size: 13px; color: var(--label-secondary); margin-top: 2px; }
.info-chevron { color: var(--label-tertiary); font-size: 14px; }
/* ── Upload zone ──────────────────────────────────────────────────────── */
/* Upload zone */
.upload-zone {
border: 2px dashed rgba(0,122,255,0.35);
border-radius: var(--radius);
@@ -268,7 +277,7 @@
.upload-title { font-size: 15px; font-weight: 600; color: var(--tint); }
.upload-sub { font-size: 13px; color: var(--label-secondary); }
/* ── Summary list ────────────────────────────────────────────────────── */
/* Summary list */
.summary-row {
display: flex;
justify-content: space-between;
@@ -280,7 +289,7 @@
.summary-key { font-size: 15px; color: var(--label-secondary); }
.summary-val { font-size: 15px; font-weight: 500; color: var(--label); }
/* ── Mission card ────────────────────────────────────────────────────── */
/* Mission card */
.mission-card {
background: var(--bg-card);
border-radius: var(--radius-lg);
@@ -307,7 +316,7 @@
.mission-detail-key { font-size: 14px; color: var(--label-secondary); }
.mission-detail-val { font-size: 15px; font-weight: 600; color: var(--label); }
/* ── CTA / Primary Button ────────────────────────────────────────────── */
/* CTA / Primary Button */
.cta-area {
padding: 16px 16px calc(var(--safe-bottom) + 24px);
background: transparent;
@@ -348,7 +357,7 @@
}
.btn-secondary:active { opacity: 0.5; }
/* ── BankID specific ─────────────────────────────────────────────────── */
/* BankID specific */
.bankid-frame {
margin: 12px 16px;
background: var(--bg-card);
@@ -361,7 +370,7 @@
width: 72px;
height: 72px;
background: linear-gradient(135deg, #235971, #4E9CC0);
border-radius: 18px;
border-radius: var(--radius-lg);
display: flex;
align-items: center;
justify-content: center;
@@ -380,20 +389,20 @@
line-height: 1.5;
}
/* ── Spinner ──────────────────────────────────────────────────────────── */
/* Spinner */
@keyframes spin { to { transform: rotate(360deg); } }
.spinner-inline {
display: inline-block;
width: 20px; height: 20px;
border: 2px solid rgba(255,255,255,0.4);
border-top-color: #fff;
border-radius: 50%;
border-radius: var(--radius-full);
animation: spin 0.7s linear infinite;
vertical-align: middle;
margin-right: 8px;
}
/* ── Success screen ──────────────────────────────────────────────────── */
/* Success screen */
@keyframes successPop {
0% { transform: scale(0.4); opacity: 0; }
70% { transform: scale(1.15); }
@@ -406,7 +415,7 @@
}
.success-circle {
width: 96px; height: 96px;
border-radius: 50%;
border-radius: var(--radius-full);
background: var(--green);
display: flex; align-items: center; justify-content: center;
font-size: 48px;
@@ -414,7 +423,7 @@
animation: successPop 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
}
/* ── Equipment chips ─────────────────────────────────────────────────── */
/* Equipment chips */
.chip-grid {
display: flex;
flex-wrap: wrap;
@@ -442,7 +451,7 @@
.equip-chip-icon { font-size: 22px; }
.equip-chip-label { font-size: 14px; font-weight: 500; color: var(--label); }
/* ── Section label ───────────────────────────────────────────────────── */
/* Section label */
.form-section-title {
font-size: 13px;
font-weight: 600;
@@ -495,11 +504,13 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<body>
<h1 style="position:absolute;left:-9999px;">Zoomer Onboarding Flow</h1>
<!-- ══════════════════════════════════════════════════════════════════════ -->
<!-- SCREEN 1: Welcome ────────────────────────────────────────────────── -->
<!-- -->
<!-- SCREEN 1: Welcome -->
<div class="screen active" id="screen-1">
<div class="hero" style="padding-top: calc(var(--safe-top) + 48px)">
<span class="hero-emoji"></span>
<div class="hero-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
</div>
<div class="hero-title">Become a Zoomer</div>
<div class="hero-sub">Earn money with your photography skills. Accept missions, deliver results, get paid fast.</div>
</div>
@@ -507,7 +518,9 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="content">
<div class="card">
<div class="info-row">
<div class="info-icon" style="background:#007AFF22">📸</div>
<div class="info-icon" style="background:#007AFF22">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#007AFF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg>
</div>
<div class="info-body">
<div class="info-title">Photograph missions</div>
<div class="info-sub">Shops, products, events in your city</div>
@@ -515,7 +528,9 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<span class="info-chevron"></span>
</div>
<div class="info-row">
<div class="info-icon" style="background:#34C75922">💰</div>
<div class="info-icon" style="background:#34C75922">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#34C759" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>
</div>
<div class="info-body">
<div class="info-title">Fast payout</div>
<div class="info-sub">Direct to your account via Stripe Connect</div>
@@ -523,7 +538,9 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<span class="info-chevron"></span>
</div>
<div class="info-row">
<div class="info-icon" style="background:#FF950022">🗺️</div>
<div class="info-icon" style="background:#FF950022">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#FF9500" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10z"/></svg>
</div>
<div class="info-body">
<div class="info-title">Your choice</div>
<div class="info-sub">Only accept missions that suit you</div>
@@ -539,8 +556,8 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
</div>
<!-- ══════════════════════════════════════════════════════════════════════ -->
<!-- SCREEN 2: BankID Verification ────────────────────────────────────── -->
<!-- -->
<!-- SCREEN 2: BankID Verification -->
<div class="screen" id="screen-2">
<div class="nav-bar">
<button class="nav-back" onclick="goBack(1)"><span class="nav-back-arrow"></span> Back</button>
@@ -551,13 +568,17 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="content">
<div class="hero" style="padding-top:20px">
<span class="hero-emoji">🏦</span>
<div class="hero-icon" style="background: linear-gradient(135deg, #235971, #4E9CC0);">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
</div>
<div class="hero-title">Verify with BankID</div>
<div class="hero-sub">We need to verify your identity so you can receive payments.</div>
</div>
<div class="bankid-frame">
<div class="bankid-logo">🔐</div>
<div class="bankid-logo">
<svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
</div>
<div class="bankid-title">Open the BankID app</div>
<div class="bankid-sub">Tap the button below to start BankID sign-in on this device.</div>
</div>
@@ -577,8 +598,8 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
</div>
<!-- ══════════════════════════════════════════════════════════════════════ -->
<!-- SCREEN 3: Profile Setup ──────────────────────────────────────────── -->
<!-- -->
<!-- SCREEN 3: Profile Setup -->
<div class="screen" id="screen-3">
<div class="nav-bar">
<button class="nav-back" onclick="goBack(2)"><span class="nav-back-arrow"></span> Back</button>
@@ -589,7 +610,9 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="content">
<div class="hero" style="padding-top:20px">
<span class="hero-emoji">👤</span>
<div class="hero-icon" style="background: linear-gradient(135deg, #AF52DE, #5856D6);">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
</div>
<div class="hero-title">Tell us about yourself</div>
<div class="hero-sub">Clients see your profile when choosing a Zoomer for their missions.</div>
</div>
@@ -631,8 +654,8 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
</div>
<!-- ══════════════════════════════════════════════════════════════════════ -->
<!-- SCREEN 4: Equipment Declaration ──────────────────────────────────── -->
<!-- -->
<!-- SCREEN 4: Equipment Declaration -->
<div class="screen" id="screen-4">
<div class="nav-bar">
<button class="nav-back" onclick="goBack(3)"><span class="nav-back-arrow"></span> Back</button>
@@ -643,28 +666,38 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="content">
<div class="hero" style="padding-top:20px">
<span class="hero-emoji">📷</span>
<div class="hero-icon" style="background: linear-gradient(135deg, #FF9500, #FFCC00);">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg>
</div>
<div class="hero-title">What equipment do you have?</div>
<div class="hero-sub">Select all equipment you can use on missions. More options more missions.</div>
<div class="hero-sub">Select all equipment you can use on missions. More options means more missions.</div>
</div>
<div class="form-section-title">Camera</div>
<div class="card">
<div class="chip-grid">
<div class="equip-chip selected" onclick="toggleChip(this)">
<span class="equip-chip-icon">📱</span>
<span class="equip-chip-icon">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="2" width="14" height="20" rx="2"/><line x1="12" y1="18" x2="12.01" y2="18"/></svg>
</span>
<span class="equip-chip-label">Smartphone</span>
</div>
<div class="equip-chip" onclick="toggleChip(this)">
<span class="equip-chip-icon">📸</span>
<span class="equip-chip-icon">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg>
</span>
<span class="equip-chip-label">Mirrorless</span>
</div>
<div class="equip-chip" onclick="toggleChip(this)">
<span class="equip-chip-icon">🎥</span>
<span class="equip-chip-icon">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg>
</span>
<span class="equip-chip-label">DSLR</span>
</div>
<div class="equip-chip" onclick="toggleChip(this)">
<span class="equip-chip-icon">🚁</span>
<span class="equip-chip-icon">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
</span>
<span class="equip-chip-label">Drone</span>
</div>
</div>
@@ -674,15 +707,21 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="card">
<div class="chip-grid">
<div class="equip-chip" onclick="toggleChip(this)">
<span class="equip-chip-icon">🕯️</span>
<span class="equip-chip-icon">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>
</span>
<span class="equip-chip-label">Flash</span>
</div>
<div class="equip-chip" onclick="toggleChip(this)">
<span class="equip-chip-icon">🔦</span>
<span class="equip-chip-icon">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><path d="M12 1v2"/><path d="M12 21v2"/><path d="M4.22 4.22l1.42 1.42"/><path d="M18.36 18.36l1.42 1.42"/><path d="M1 12h2"/><path d="M21 12h2"/><path d="M4.22 19.78l1.42-1.42"/><path d="M18.36 5.64l1.42-1.42"/></svg>
</span>
<span class="equip-chip-label">Lighting</span>
</div>
<div class="equip-chip" onclick="toggleChip(this)">
<span class="equip-chip-icon">📐</span>
<span class="equip-chip-icon">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
</span>
<span class="equip-chip-label">Tripod</span>
</div>
</div>
@@ -712,8 +751,8 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
</div>
<!-- ══════════════════════════════════════════════════════════════════════ -->
<!-- SCREEN 5: KYC Document Upload ────────────────────────────────────── -->
<!-- -->
<!-- SCREEN 5: KYC Document Upload -->
<div class="screen" id="screen-5">
<div class="nav-bar">
<button class="nav-back" onclick="goBack(4)"><span class="nav-back-arrow"></span> Back</button>
@@ -724,7 +763,9 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="content">
<div class="hero" style="padding-top:20px">
<span class="hero-emoji">🪪</span>
<div class="hero-icon" style="background: linear-gradient(135deg, #34C759, #30D158);">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
</div>
<div class="hero-title">Upload ID document</div>
<div class="hero-sub">Required by law to verify your identity and enable payouts.</div>
</div>
@@ -732,7 +773,9 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="form-section-title">Accepted documents</div>
<div class="card">
<div class="info-row" onclick="markDoc('passport', this)" style="cursor:pointer">
<div class="info-icon" style="background:#007AFF22" id="doc-passport-icon">📘</div>
<div class="info-icon" style="background:#007AFF22" id="doc-passport-icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#007AFF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
</div>
<div class="info-body">
<div class="info-title">Passport</div>
<div class="info-sub">Valid passport with photo page</div>
@@ -740,7 +783,9 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<span class="info-chevron" id="doc-passport-check"></span>
</div>
<div class="info-row" onclick="markDoc('id', this)" style="cursor:pointer">
<div class="info-icon" style="background:#AF52DE22" id="doc-id-icon">🪪</div>
<div class="info-icon" style="background:#AF52DE22" id="doc-id-icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#AF52DE" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
</div>
<div class="info-body">
<div class="info-title">National ID card</div>
<div class="info-sub">Front and back</div>
@@ -748,7 +793,9 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<span class="info-chevron" id="doc-id-check"></span>
</div>
<div class="info-row" onclick="markDoc('license', this)" style="cursor:pointer">
<div class="info-icon" style="background:#34C75922" id="doc-license-icon">🚗</div>
<div class="info-icon" style="background:#34C75922" id="doc-license-icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#34C759" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
</div>
<div class="info-body">
<div class="info-title">Driving licence</div>
<div class="info-sub">Valid driving licence</div>
@@ -758,9 +805,11 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
<div class="upload-zone" onclick="simulateUpload()">
<span class="upload-icon" id="upload-icon">📎</span>
<span class="upload-icon" id="upload-icon">
<svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="#007AFF" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
</span>
<span class="upload-title" id="upload-title">Choose file or take photo</span>
<span class="upload-sub" id="upload-sub">JPG, PNG eller PDF · Max 10 MB</span>
<span class="upload-sub" id="upload-sub">JPG, PNG or PDF · Max 10 MB</span>
</div>
<div class="form-section-title">Bank details</div>
@@ -781,8 +830,8 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
</div>
<!-- ══════════════════════════════════════════════════════════════════════ -->
<!-- SCREEN 6: First Mission ──────────────────────────────────────────── -->
<!-- -->
<!-- SCREEN 6: First Mission -->
<div class="screen" id="screen-6">
<div class="nav-bar">
<button class="nav-back" onclick="goBack(5)"><span class="nav-back-arrow"></span> Back</button>
@@ -793,16 +842,21 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="content" style="padding:0">
<div class="hero" style="padding-top:20px">
<span class="hero-emoji">🎯</span>
<div class="hero-icon" style="background: linear-gradient(135deg, #FF3B30, #FF6B6B);">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/></svg>
</div>
<div class="hero-title">Ready for your first mission?</div>
<div class="hero-sub">We found a mission near you!</div>
</div>
<div class="mission-card">
<div class="mission-header">
<div class="mission-type">📸 Store photography</div>
<div class="mission-type">Store photography</div>
<div class="mission-title">H&M Flagship — Stockholm</div>
<div class="mission-loc">📍 Drottninggatan 55, Stockholm · 1.2 km away</div>
<div class="mission-loc">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:inline;vertical-align:middle;margin-right:4px"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>
Drottninggatan 55, Stockholm · 1.2 km away
</div>
</div>
<div class="mission-detail">
<span class="mission-detail-key">Compensation</span>
@@ -834,7 +888,10 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
<div class="summary-row">
<span class="summary-key">Rating</span>
<span class="summary-val">⭐ 4.9 (47 missions)</span>
<span class="summary-val">
<svg width="14" height="14" viewBox="0 0 24 24" fill="#FF9500" stroke="#FF9500" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:inline;vertical-align:middle;margin-right:4px"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
4.9 (47 missions)
</span>
</div>
<div class="summary-row">
<span class="summary-key">Payout</span>
@@ -845,21 +902,26 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
<div class="cta-area">
<button class="btn-primary green" onclick="goTo(7)">✓ Accept mission</button>
<button class="btn-primary green" onclick="goTo(7)">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" style="display:inline;vertical-align:middle;margin-right:6px"><polyline points="20 6 9 17 4 12"/></svg>
Accept mission
</button>
<button class="btn-secondary" onclick="goTo(7)">View more missions</button>
</div>
</div>
<!-- ══════════════════════════════════════════════════════════════════════ -->
<!-- SCREEN 7: Success ────────────────────────────────────────────────── -->
<!-- -->
<!-- SCREEN 7: Success -->
<div class="screen" id="screen-7">
<div class="nav-bar" style="justify-content:center">
<div class="nav-title-center" style="color:var(--green)">🎉 Welcome!</div>
<div class="nav-title-center" style="color:var(--green)">Welcome!</div>
</div>
<div class="content" style="display:flex; flex-direction:column; align-items:center; text-align:center; padding-top:0">
<div class="success-icon-wrap">
<div class="success-circle"></div>
<div class="success-circle">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
</div>
</div>
<div style="padding:0 28px 20px">
@@ -871,15 +933,24 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="card">
<div class="summary-row">
<span class="summary-key">Status</span>
<span class="summary-val" style="color:var(--green)">✓ Active Zoomer</span>
<span class="summary-val" style="color:var(--green);display:flex;align-items:center;gap:6px">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#34C759" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
Active Zoomer
</span>
</div>
<div class="summary-row">
<span class="summary-key">BankID</span>
<span class="summary-val" style="color:var(--green)">✓ Verifierad</span>
<span class="summary-val" style="color:var(--green);display:flex;align-items:center;gap:6px">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#34C759" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
Verified
</span>
</div>
<div class="summary-row">
<span class="summary-key">KYC</span>
<span class="summary-val" style="color:var(--yellow)">⏳ Review in progress</span>
<span class="summary-val" style="color:var(--yellow);display:flex;align-items:center;gap:6px">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#FF9500" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
Review in progress
</span>
</div>
<div class="summary-row">
<span class="summary-key">First mission</span>
@@ -893,7 +964,8 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
<div style="padding:24px 28px 0; color:var(--label-secondary); font-size:14px; line-height:1.6">
📲 You will receive a confirmation email. Open the quiXzoom app to see details about your mission.
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:inline;vertical-align:middle;margin-right:4px"><rect x="5" y="2" width="14" height="20" rx="2"/><line x1="12" y1="18" x2="12.01" y2="18"/></svg>
You will receive a confirmation email. Open the quiXzoom app to see details about your mission.
</div>
</div>
@@ -904,12 +976,12 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
<script>
// ── State ──────────────────────────────────────────────────────────────
// State
const TOTAL_STEPS = 6; // screens 2-7
let currentScreen = 1;
let bankIDTimer = null;
// ── Navigation ─────────────────────────────────────────────────────────
// Navigation
function goTo(n, direction = 'right') {
const curr = document.getElementById(`screen-${currentScreen}`);
const next = document.getElementById(`screen-${n}`);
@@ -930,7 +1002,7 @@ function goBack(n) {
goTo(n, 'left');
}
// ── Step dots ──────────────────────────────────────────────────────────
// Step dots
function renderSteps(screenN) {
if (screenN < 2) return;
const step = screenN - 1; // step 1..6
@@ -943,7 +1015,7 @@ function renderSteps(screenN) {
}).join('');
}
// ── BankID simulation ──────────────────────────────────────────────────
// BankID simulation
function startBankID() {
const btn = document.getElementById('bankid-btn');
btn.innerHTML = '<span class="spinner-inline"></span>Waiting for BankID…';
@@ -958,32 +1030,32 @@ function startBankID() {
}, 2500);
}
// ── Equipment chip toggle ──────────────────────────────────────────────
// Equipment chip toggle
function toggleChip(el) {
el.classList.toggle('selected');
}
// ── Document selection ─────────────────────────────────────────────────
// Document selection
let selectedDoc = null;
function markDoc(type, row) {
['passport','id','license'].forEach(t => {
document.getElementById(`doc-${t}-check`).textContent = '';
});
selectedDoc = type;
document.getElementById(`doc-${type}-check`).textContent = '';
document.getElementById(`doc-${type}-check`).textContent = '<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>';
}
// ── Upload simulation ──────────────────────────────────────────────────
// Upload simulation
function simulateUpload() {
const icon = document.getElementById('upload-icon');
const title = document.getElementById('upload-title');
const sub = document.getElementById('upload-sub');
icon.textContent = '';
icon.textContent = '<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>';
title.textContent = 'File uploaded';
sub.textContent = 'id_dokument.jpg · 2.4 MB';
}
// ── Init ──────────────────────────────────────────────────────────────
// Init
renderSteps(1);
</script>
</body>