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
+73 -73
View File
@@ -37,7 +37,7 @@
padding: 32px 20px 24px;
}
.avatar {
width: 88px; height: 88px; border-radius: 50%;
width: 88px; height: 88px; border-radius: var(--radius-full);
background: linear-gradient(135deg, var(--ios-blue), var(--ios-indigo));
display: flex; align-items: center; justify-content: center;
font-size: 42px; margin-bottom: 14px;
@@ -45,7 +45,7 @@
}
.avatar-badge {
position: absolute; bottom: 0; right: 0;
width: 26px; height: 26px; border-radius: 50%;
width: 26px; height: 26px; border-radius: var(--radius-full);
border: 2.5px solid var(--grouped-background-primary);
display: flex; align-items: center; justify-content: center;
font-size: 14px;
@@ -76,7 +76,7 @@
display: grid; grid-template-columns: 1fr 1fr 1fr;
margin: 0 16px 20px;
background: var(--background-secondary);
border-radius: 14px;
border-radius: var(--radius-lg);
border: 0.5px solid var(--separator);
overflow: hidden;
}
@@ -100,7 +100,7 @@
}
.section-card {
background: var(--background-secondary);
border-radius: 14px; overflow: hidden;
border-radius: var(--radius-lg); overflow: hidden;
border: 0.5px solid var(--separator);
}
@@ -114,7 +114,7 @@
.list-row:last-child { border-bottom: none; }
.list-row:active { opacity: 0.7; }
.row-icon-wrap {
width: 32px; height: 32px; border-radius: 8px;
width: 32px; height: 32px; border-radius: var(--radius-md);
display: flex; align-items: center; justify-content: center;
font-size: 18px; flex-shrink: 0;
}
@@ -130,7 +130,7 @@
/* KYC Card */
.kyc-card {
background: var(--background-secondary);
border-radius: 14px; overflow: hidden;
border-radius: var(--radius-lg); overflow: hidden;
border: 0.5px solid var(--separator);
}
.kyc-top {
@@ -138,7 +138,7 @@
padding: 16px;
}
.kyc-icon {
width: 52px; height: 52px; border-radius: 12px;
width: 52px; height: 52px; border-radius: var(--radius-md);
display: flex; align-items: center; justify-content: center;
font-size: 26px; flex-shrink: 0;
}
@@ -158,7 +158,7 @@
}
.status-chip {
display: inline-flex; align-items: center; gap: 4px;
padding: 4px 10px; border-radius: 20px;
padding: 4px 10px; border-radius: var(--radius-xl);
font-size: 12px; font-weight: 600;
}
.chip-unverified { background: rgba(255,149,0,0.15); color: var(--ios-orange); }
@@ -190,13 +190,13 @@
.type-label { flex: 1; font-size: 14px; }
.type-count { font-size: 14px; font-weight: 600; color: var(--label-secondary); }
.type-bar-wrap { width: 80px; }
.type-bar { height: 4px; border-radius: 2px; background: var(--ios-blue); }
.type-bar { height: 4px; border-radius: var(--radius-sm); background: var(--ios-blue); }
/* Onboarding Progress Bar */
.onboarding-card {
margin: 0 16px 20px;
background: var(--background-secondary);
border-radius: 16px; overflow: hidden;
border-radius: var(--radius-lg); overflow: hidden;
border: 0.5px solid var(--separator);
}
.onboarding-header {
@@ -207,10 +207,10 @@
.onboarding-fraction { font-size: 13px; color: var(--label-secondary); font-weight: 500; }
.onboarding-track {
height: 5px; background: var(--fill-secondary);
margin: 0 16px 14px; border-radius: 3px; overflow: hidden;
margin: 0 16px 14px; border-radius: var(--radius-sm); overflow: hidden;
}
.onboarding-fill {
height: 100%; border-radius: 3px;
height: 100%; border-radius: var(--radius-sm);
background: linear-gradient(90deg, var(--ios-blue), var(--ios-indigo));
transition: width 0.6s cubic-bezier(0.34,1.56,0.64,1);
}
@@ -232,7 +232,7 @@
margin: 0 16px 16px;
padding: 15px;
background: var(--ios-blue); color: #fff;
border: none; border-radius: 12px;
border: none; border-radius: var(--radius-md);
font-family: var(--font-text); font-size: 16px; font-weight: 700;
cursor: pointer; transition: opacity 0.15s;
letter-spacing: -0.2px; text-align: center;
@@ -243,7 +243,7 @@
/* Earnings Calculator */
.calc-card {
background: var(--background-secondary);
border-radius: 16px; overflow: hidden;
border-radius: var(--radius-lg); overflow: hidden;
border: 0.5px solid var(--separator);
}
.calc-header { padding: 14px 16px 12px; border-bottom: 0.5px solid var(--separator); }
@@ -259,19 +259,19 @@
.calc-slider {
width: 100%; margin-bottom: 16px;
-webkit-appearance: none; appearance: none;
height: 4px; border-radius: 2px;
height: 4px; border-radius: var(--radius-sm);
background: var(--ios-blue); outline: none;
}
.calc-slider::-webkit-slider-thumb {
-webkit-appearance: none; appearance: none;
width: 22px; height: 22px; border-radius: 50%;
width: 22px; height: 22px; border-radius: var(--radius-full);
background: #fff; box-shadow: 0 2px 8px rgba(0,0,0,0.25);
cursor: pointer;
}
.calc-result {
background: rgba(48,209,88,0.08);
border: 0.5px solid rgba(48,209,88,0.2);
border-radius: 12px; padding: 16px; text-align: center;
border-radius: var(--radius-md); padding: 16px; text-align: center;
}
.calc-result-label { font-size: 13px; color: var(--label-secondary); margin-bottom: 6px; }
.calc-result-amount {
@@ -293,13 +293,13 @@
.referral-card {
background: linear-gradient(135deg, rgba(10,132,255,0.08), rgba(94,92,230,0.08));
border: 0.5px solid rgba(10,132,255,0.2);
border-radius: 16px; padding: 20px;
border-radius: var(--radius-lg); padding: 20px;
}
.referral-header {
display: flex; gap: 12px; align-items: center; margin-bottom: 14px;
}
.referral-icon {
width: 48px; height: 48px; border-radius: 12px;
width: 48px; height: 48px; border-radius: var(--radius-md);
background: rgba(10,132,255,0.15);
display: flex; align-items: center; justify-content: center;
font-size: 24px;
@@ -311,14 +311,14 @@
display: flex; gap: 10px; align-items: stretch; margin-bottom: 14px;
}
.referral-code {
flex: 1; background: var(--fill-secondary); border-radius: 10px;
flex: 1; background: var(--fill-secondary); border-radius: var(--radius-md);
padding: 12px 14px;
font-family: var(--font-mono); font-size: 16px; font-weight: 700;
letter-spacing: 2px; color: var(--ios-blue); text-align: center;
}
.referral-copy-btn {
background: var(--ios-blue); color: #fff;
border: none; border-radius: 10px;
border: none; border-radius: var(--radius-md);
padding: 12px 16px; font-size: 14px; font-weight: 700;
cursor: pointer; transition: opacity 0.15s;
}
@@ -326,7 +326,7 @@
.referral-share-btn {
display: block; width: 100%;
padding: 13px; background: rgba(10,132,255,0.12); color: var(--ios-blue);
border: 0.5px solid rgba(10,132,255,0.2); border-radius: 10px;
border: 0.5px solid rgba(10,132,255,0.2); border-radius: var(--radius-md);
font-size: 15px; font-weight: 600;
cursor: pointer; transition: opacity 0.15s; text-align: center;
margin-bottom: 12px;
@@ -336,7 +336,7 @@
display: flex; gap: 10px;
}
.referral-stat {
flex: 1; background: var(--fill-secondary); border-radius: 10px;
flex: 1; background: var(--fill-secondary); border-radius: var(--radius-md);
padding: 10px; text-align: center;
}
.referral-stat-num { font-size: 20px; font-weight: 700; font-family: var(--font-display); }
@@ -345,7 +345,7 @@
/* Loader */
@keyframes spin { to { transform: rotate(360deg); } }
.spinner { width:24px; height:24px; border-radius:50%; border:3px solid var(--fill-secondary); border-top-color:var(--ios-blue); animation:spin 0.8s linear infinite; display:inline-block; }
.spinner { width:24px; height:24px; border-radius: var(--radius-full); border:3px solid var(--fill-secondary); border-top-color:var(--ios-blue); animation:spin 0.8s linear infinite; display:inline-block; }
/* Tab Bar */
.tab-bar {
@@ -363,7 +363,7 @@
.tab-item.active .tab-label { color:var(--ios-blue); }
/* Toast */
.toast { position:fixed; bottom:calc(env(safe-area-inset-bottom,34px)+100px); left:50%; transform:translateX(-50%) translateY(20px); background:var(--background-secondary); border:0.5px solid var(--separator); border-radius:20px; padding:10px 18px; font-size:14px; font-weight:500; box-shadow:0 8px 32px rgba(0,0,0,0.3); opacity:0; transition:all 0.3s; z-index:9999; white-space:nowrap; }
.toast { position:fixed; bottom:calc(env(safe-area-inset-bottom,34px)+100px); left:50%; transform:translateX(-50%) translateY(20px); background:var(--background-secondary); border:0.5px solid var(--separator); border-radius: var(--radius-xl); padding:10px 18px; font-size:14px; font-weight:500; box-shadow:0 8px 32px rgba(0,0,0,0.3); opacity:0; transition:all 0.3s; z-index:9999; white-space:nowrap; }
.toast.show { opacity:1; transform:translateX(-50%) translateY(0); }
@media (max-width: 640px) {
.hero h1 { font-size: 2rem !important; }
@@ -417,13 +417,13 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<!-- Profile Hero -->
<div class="profile-hero">
<div class="avatar" id="avatar">
🧑
<div class="avatar-badge unverified" id="avatar-badge">⚠️</div>
<div class="avatar-badge unverified" id="avatar-badge"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg></div>
</div>
<div class="profile-name" id="profile-name">Zoomer</div>
<div class="profile-id" id="profile-id">Laddar...</div>
<div class="rating-row">
<span class="stars">★★★★★</span>
<span class="stars"></span>
<span class="rating-num" id="rating-num"></span>
<span class="rating-count" id="rating-count">(0 uppdrag)</span>
</div>
@@ -448,7 +448,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<!-- Onboarding Progress -->
<div class="onboarding-card" id="onboarding-section">
<div class="onboarding-header">
<span class="onboarding-title">🚀 Kom igång</span>
<span class="onboarding-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> Kom igång</span>
<span class="onboarding-fraction" id="ob-fraction">0 / 3 klara</span>
</div>
<div class="onboarding-track">
@@ -456,15 +456,15 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
<div class="onboarding-steps">
<div class="onboarding-step" id="ob-step-1">
<div class="os-icon">🪪</div>
<div class="os-icon"></div>
<div class="os-label" id="ob-lbl-1">KYC</div>
</div>
<div class="onboarding-step" id="ob-step-2">
<div class="os-icon">🏦</div>
<div class="os-icon"></div>
<div class="os-label" id="ob-lbl-2">Utbetalning</div>
</div>
<div class="onboarding-step" id="ob-step-3">
<div class="os-icon">🎯</div>
<div class="os-icon"></div>
<div class="os-label" id="ob-lbl-3">Första uppdrag</div>
</div>
</div>
@@ -476,18 +476,18 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="section-header">Identitetsverifiering</div>
<div class="kyc-card">
<div class="kyc-top">
<div class="kyc-icon unverified" id="kyc-icon">🪪</div>
<div class="kyc-icon unverified" id="kyc-icon"></div>
<div class="kyc-info">
<div class="kyc-title">Identitetsverifiering</div>
<div class="kyc-desc">Verifiera din identitet för att ta emot utbetalningar. Krävs enligt lag (AML).</div>
</div>
</div>
<div class="kyc-status-bar" id="kyc-status-bar">
<span class="status-chip chip-unverified" id="kyc-chip">⚠️ Ej verifierad</span>
<span class="status-chip chip-unverified" id="kyc-chip"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg> Ej verifierad</span>
<span style="color:var(--label-secondary)">Verifiera för att låsa upp alla uppdrag</span>
</div>
<button class="kyc-btn unverified" id="kyc-btn" onclick="startKYC()">
🪪 Verifiera identitet →
Verifiera identitet →
</button>
</div>
</div>
@@ -497,7 +497,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="section-header">Inkomstpotential</div>
<div class="calc-card">
<div class="calc-header">
<div class="calc-title">💰 Vad kan du tjäna?</div>
<div class="calc-title"> Vad kan du tjäna?</div>
<div class="calc-sub">Estimera din månadsinkomst</div>
</div>
<div class="calc-body">
@@ -510,12 +510,12 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<span class="calc-label">Uppdragstyp</span>
<span class="calc-value" id="calc-type-lbl">Fasadscanning (800 kr)</span>
</div>
<select id="calc-type" onchange="updateCalc()" style="width:100%;background:var(--fill-secondary);border:0.5px solid var(--separator);border-radius:10px;padding:10px 14px;font-size:14px;color:var(--label-primary);font-family:var(--font-text);margin-bottom:16px;-webkit-appearance:none">
<option value="800">🏢 Fasadscanning — 800 kr</option>
<option value="1200">🏠 Takscanning — 1 200 kr</option>
<option value="600">🌳 Vegetationsinventering — 600 kr</option>
<option value="1500">🛣️ Väginspektionen — 1 500 kr</option>
<option value="2000">🏗️ Byggnadsdokumentation — 2 000 kr</option>
<select id="calc-type" onchange="updateCalc()" style="width:100%;background:var(--fill-secondary);border:0.5px solid var(--separator);border-radius: var(--radius-md);padding:10px 14px;font-size:14px;color:var(--label-primary);font-family:var(--font-text);margin-bottom:16px;-webkit-appearance:none">
<option value="800"><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 5H7M6 8H7M6 11H7M9 5H10M9 8H10M9 11H10" stroke="#666" stroke-width="1.5" stroke-linecap="round"/></svg> Fasadscanning — 800 kr</option>
<option value="1200"> Takscanning — 1 200 kr</option>
<option value="600"> Vegetationsinventering — 600 kr</option>
<option value="1500"> Väginspektionen — 1 500 kr</option>
<option value="2000"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M2 14H14M4 14V8L8 4L12 8V14" stroke="#666" stroke-width="1.5" stroke-linejoin="round"/><path d="M6 14V10H10V14" stroke="#666" stroke-width="1.5"/></svg> Byggnadsdokumentation — 2 000 kr</option>
</select>
<div class="calc-result">
<div class="calc-result-label">Estimerad månadsinkomst</div>
@@ -545,7 +545,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="section-header">Referral Program</div>
<div class="referral-card">
<div class="referral-header">
<div class="referral-icon">🤝</div>
<div class="referral-icon"></div>
<div class="referral-info">
<div class="referral-title">Tjäna 200 kr per ny Zoomer</div>
<div class="referral-desc">Dela din kod — du och din vän får 200 kr när de slutför sitt första uppdrag</div>
@@ -553,9 +553,9 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
<div class="referral-code-row">
<div class="referral-code" id="referral-code">ZOOM-——</div>
<button class="referral-copy-btn" onclick="copyReferralCode()">📋 Kopiera</button>
<button class="referral-copy-btn" onclick="copyReferralCode()"><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</button>
</div>
<button class="referral-share-btn" onclick="shareReferral()">📤 Dela via iOS Share Sheet</button>
<button class="referral-share-btn" onclick="shareReferral()"> Dela via iOS Share Sheet</button>
<div class="referral-stats">
<div class="referral-stat">
<div class="referral-stat-num" id="ref-count">0</div>
@@ -586,7 +586,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="section-header">Inställningar</div>
<div class="section-card">
<div class="list-row" onclick="copyId()">
<div class="row-icon-wrap" style="background:rgba(0,122,255,0.15)">🆔</div>
<div class="row-icon-wrap" style="background:rgba(0,122,255,0.15)"></div>
<div class="row-body">
<div class="row-title">Zoomer-ID</div>
<div class="row-sub" id="full-id" style="font-family:var(--font-mono);font-size:11px"></div>
@@ -594,7 +594,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<span class="row-right">Kopiera <span class="chevron"></span></span>
</div>
<div class="list-row" onclick="openEarnings()">
<div class="row-icon-wrap" style="background:rgba(52,199,89,0.15)">💰</div>
<div class="row-icon-wrap" style="background:rgba(52,199,89,0.15)"></div>
<div class="row-body">
<div class="row-title">Intjäning & utbetalningar</div>
<div class="row-sub">Utbetalningskonto, historik</div>
@@ -602,7 +602,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<span class="chevron"></span>
</div>
<div class="list-row" onclick="openSupport()">
<div class="row-icon-wrap" style="background:rgba(175,82,222,0.15)">💬</div>
<div class="row-icon-wrap" style="background:rgba(175,82,222,0.15)"></div>
<div class="row-body">
<div class="row-title">Support</div>
<div class="row-sub">support@quixzoom.com</div>
@@ -610,7 +610,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<span class="chevron"></span>
</div>
<div class="list-row" onclick="openTerms()">
<div class="row-icon-wrap" style="background:rgba(142,142,147,0.15)">📄</div>
<div class="row-icon-wrap" style="background:rgba(142,142,147,0.15)"></div>
<div class="row-body">
<div class="row-title">Användarvillkor & Integritet</div>
<div class="row-sub">Juridisk information</div>
@@ -631,15 +631,15 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<!-- Tab Bar -->
<nav class="tab-bar">
<a class="tab-item" href="zoomer-app.html">
<span class="tab-icon">🎯</span>
<span class="tab-icon"></span>
<span class="tab-label">Uppdrag</span>
</a>
<a class="tab-item" href="zoomer-earnings.html">
<span class="tab-icon">💰</span>
<span class="tab-icon"></span>
<span class="tab-label">Intjäning</span>
</a>
<a class="tab-item active" href="zoomer-profile.html">
<span class="tab-icon">👤</span>
<span class="tab-icon"></span>
<span class="tab-label">Profil</span>
</a>
</nav>
@@ -675,8 +675,8 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
return m[type] || type;
}
function missionTypeEmoji(type) {
const m = { facade_scan:'🏢', roof_scan:'🏠', road_inspection:'🛣️', interior_scan:'🏚️', vegetation_survey:'🌳', inspection:'🔍' };
return m[type] || '📍';
const m = { facade_scan:'<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 5H7M6 8H7M6 11H7M9 5H10M9 8H10M9 11H10" stroke="#666" stroke-width="1.5" stroke-linecap="round"/></svg>', roof_scan:'', road_inspection:'', interior_scan:'', vegetation_survey:'', inspection:'<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><circle cx="7" cy="7" r="5" stroke="#666" stroke-width="2"/><path d="M11 11L14 14" stroke="#666" stroke-width="2" stroke-linecap="round"/></svg>' };
return m[type] || '';
}
async function loadProfile() {
@@ -743,13 +743,13 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
if (status === 'verified') {
iconEl.className = 'kyc-icon verified';
iconEl.textContent = '';
iconEl.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>';
chipEl.className = 'status-chip chip-verified';
chipEl.textContent = ' Verifierad';
chipEl.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> Verifierad';
btnEl.className = 'kyc-btn verified';
btnEl.textContent = ' Identitet verifierad';
btnEl.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> Identitet verifierad';
badgeEl.className = 'avatar-badge verified';
badgeEl.textContent = '';
badgeEl.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>';
document.getElementById('kyc-status-bar').querySelector('span:last-child').textContent = 'Konto verifierat — alla uppdrag tillgängliga';
} else if (status === 'pending') {
iconEl.className = 'kyc-icon pending';
@@ -762,11 +762,11 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
} else {
// unverified
iconEl.className = 'kyc-icon unverified';
iconEl.textContent = '🪪';
iconEl.textContent = '';
chipEl.className = 'status-chip chip-unverified';
chipEl.textContent = '⚠️ Ej verifierad';
chipEl.textContent = '<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg> Ej verifierad';
btnEl.className = 'kyc-btn unverified';
btnEl.textContent = '🪪 Verifiera identitet →';
btnEl.textContent = ' Verifiera identitet →';
}
}
@@ -784,7 +784,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
setKYCStatus('pending');
renderKYCStatus('pending');
toast('🪪 Öppnar verifiering...');
toast(' Öppnar verifiering...');
window.open(kycUrl, '_blank');
// Starta polling för status
@@ -792,8 +792,8 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
} catch(e) {
console.error('KYC error:', e);
toast(' Ett fel uppstod: ' + e.message);
btn.textContent = '🪪 Verifiera identitet →';
toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#ef4444" stroke-width="2"/><path d="M7 7L13 13M13 7L7 13" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg> Ett fel uppstod: ' + e.message);
btn.textContent = ' Verifiera identitet →';
btn.disabled = false;
}
}
@@ -808,12 +808,12 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
if (data.assurance_level === 'photo_low' || data.status === 'approved') {
setKYCStatus('verified');
renderKYCStatus('verified');
toast(' Identitet verifierad!');
toast('<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> Identitet verifierad!');
return;
} else if (data.assurance_level === 'rejected') {
setKYCStatus('unverified');
renderKYCStatus('unverified');
toast(' Verifiering nekad: ' + (data.reject_reason || 'Okänt fel'));
toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#ef4444" stroke-width="2"/><path d="M7 7L13 13M13 7L7 13" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg> Verifiering nekad: ' + (data.reject_reason || 'Okänt fel'));
return;
}
@@ -827,7 +827,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
function copyId() {
const id = getZoomerId();
navigator.clipboard?.writeText(id).then(() => toast('📋 ID kopierat!')).catch(() => toast('ID: ' + id));
navigator.clipboard?.writeText(id).then(() => toast('<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> ID kopierat!')).catch(() => toast('ID: ' + id));
}
function openEarnings() { window.location.href = 'zoomer-earnings.html'; }
@@ -858,7 +858,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
steps.forEach((s,i) => {
const el = document.getElementById(labels[i]);
el.className = 'os-label ' + (s ? 'done' : (i === done ? 'active' : ''));
if (s) el.textContent = ' ' + texts[i];
if (s) el.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> ' + texts[i];
else if (i === done) el.textContent = ' ' + texts[i];
else el.textContent = texts[i];
});
@@ -867,13 +867,13 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
if (done >= 2) {
// KYC + bank done — can take first mission
cta.disabled = false;
cta.textContent = steps[2] ? ' Första uppdrag slutfört!' : '🎯 Ta mitt första uppdrag →';
cta.textContent = steps[2] ? '<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> Första uppdrag slutfört!' : ' Ta mitt första uppdrag →';
if (steps[2]) cta.disabled = true;
} else {
cta.disabled = true;
cta.textContent = done === 0 ? '🔒 Slutför KYC för att börja' :
done === 1 ? '🔒 Länka bankkonto för att fortsätta' :
'🎯 Ta mitt första uppdrag →';
cta.textContent = done === 0 ? ' Slutför KYC för att börja' :
done === 1 ? ' Länka bankkonto för att fortsätta' :
' Ta mitt första uppdrag →';
}
}
updateOnboarding();
@@ -932,7 +932,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
function copyReferralCode() {
const code = getReferralCode();
const msg = 'Gå med i quiXzoom med min kod ' + code + ' och tjäna 200 kr på ditt första uppdrag! https://quixzoom.com/join?ref=' + code;
navigator.clipboard?.writeText(msg).then(() => toast('📋 Referral-kod kopierad!')).catch(() => toast('Kod: ' + code));
navigator.clipboard?.writeText(msg).then(() => toast('<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> Referral-kod kopierad!')).catch(() => toast('Kod: ' + code));
}
function shareReferral() {