From f3802785f376904bf7caa711b2bb2d56d3ef8659 Mon Sep 17 00:00:00 2001 From: Johan Lindengard Date: Thu, 7 May 2026 15:46:34 +0200 Subject: [PATCH 1/2] fix(ios): align app config with staging and production environments Co-Authored-By: Claude Opus 4.7 (1M context) --- ios/Equinet/Equinet/AppConfig.swift | 38 ++++--- ios/Equinet/EquinetTests/AppConfigTests.swift | 99 +++++++++++++++++++ 2 files changed, 121 insertions(+), 16 deletions(-) create mode 100644 ios/Equinet/EquinetTests/AppConfigTests.swift diff --git a/ios/Equinet/Equinet/AppConfig.swift b/ios/Equinet/Equinet/AppConfig.swift index 67b2e251..2f9134c3 100644 --- a/ios/Equinet/Equinet/AppConfig.swift +++ b/ios/Equinet/Equinet/AppConfig.swift @@ -14,8 +14,8 @@ import Foundation enum AppEnvironment: String { case local // localhost + local Supabase (supabase start) - case staging // Vercel + remote Supabase staging - case production // Vercel + remote Supabase (same as staging until separate prod project) + case staging // Vercel staging custom domain + staging Supabase + case production // Vercel prod custom domain + prod Supabase static var current: AppEnvironment { #if DEBUG @@ -37,19 +37,19 @@ enum AppConfig { // MARK: Web server - static var baseURL: URL { - switch AppEnvironment.current { + static func baseURL(for env: AppEnvironment) -> URL { + switch env { case .local: return URL(string: "http://localhost:3000")! case .staging: - // Stabil Vercel branch-preview för staging-branchen. - // URL-mönster: equinet-git--.vercel.app — uppdatera vid org-flytt. - return URL(string: "https://equinet-git-staging-cola500.vercel.app")! + return URL(string: "https://equinet-staging.johanlindengard.com")! case .production: - return URL(string: "https://equinet-app.vercel.app")! + return URL(string: "https://equinet.johanlindengard.com")! } } + static var baseURL: URL { baseURL(for: .current) } + /// Start URL -- skip landing page, go straight to login/dashboard static var startURL: URL { URL(string: "login", relativeTo: baseURL)! @@ -64,27 +64,33 @@ enum AppConfig { // MARK: - Supabase Auth - static var supabaseURL: URL { - switch AppEnvironment.current { + static func supabaseURL(for env: AppEnvironment) -> URL { + switch env { case .local: return URL(string: "http://127.0.0.1:54321")! - case .staging, .production: - // Both use staging project (zzdamokfeenencuggjjp) until Apple Developer - // Program is purchased and separate prod bundle ID + project is created. + case .staging: return URL(string: "https://zzdamokfeenencuggjjp.supabase.co")! + case .production: + return URL(string: "https://xybyzflfxnqqyxnvjklv.supabase.co")! } } - static var supabaseAnonKey: String { - switch AppEnvironment.current { + static var supabaseURL: URL { supabaseURL(for: .current) } + + static func supabaseAnonKey(for env: AppEnvironment) -> String { + switch env { case .local: // Standard supabase start demo key (same for all local projects) return "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0" - case .staging, .production: + case .staging: return "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inp6ZGFtb2tmZWVuZW5jdWdnampwIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzUxNDE1NjQsImV4cCI6MjA5MDcxNzU2NH0.ugROkgUOuq1fLte2wbt16TDugfUZW7qYro-nQgobVxQ" + case .production: + return "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inh5Ynl6ZmxmeG5xcXl4bnZqa2x2Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3Njg5Njk3NDAsImV4cCI6MjA4NDU0NTc0MH0.wlkcjzDthqvRV-6cV-pkNd8JQ7WFCYC5kznFeVZRRlc" } } + static var supabaseAnonKey: String { supabaseAnonKey(for: .current) } + /// Server endpoint for exchanging Supabase token to web session cookies static let sessionExchangePath = "api/auth/native-session-exchange" } diff --git a/ios/Equinet/EquinetTests/AppConfigTests.swift b/ios/Equinet/EquinetTests/AppConfigTests.swift new file mode 100644 index 00000000..e96ddbd8 --- /dev/null +++ b/ios/Equinet/EquinetTests/AppConfigTests.swift @@ -0,0 +1,99 @@ +// +// AppConfigTests.swift +// EquinetTests +// +// Verifies that .local, .staging, and .production environments map to distinct +// base URLs and Supabase project refs. Catches regressions where staging falls +// through to production or where prod accidentally points at staging Supabase. +// + +@testable import Equinet +import XCTest + +final class AppConfigTests: XCTestCase { + + // MARK: - baseURL per environment + + func testLocalBaseURLPointsToLocalhost() { + let url = AppConfig.baseURL(for: .local) + XCTAssertEqual(url.absoluteString, "http://localhost:3000") + } + + func testStagingBaseURLPointsToCustomDomain() { + let url = AppConfig.baseURL(for: .staging) + XCTAssertEqual(url.absoluteString, "https://equinet-staging.johanlindengard.com") + } + + func testProductionBaseURLPointsToCustomDomain() { + let url = AppConfig.baseURL(for: .production) + XCTAssertEqual(url.absoluteString, "https://equinet.johanlindengard.com") + } + + func testStagingAndProductionBaseURLAreDistinct() { + XCTAssertNotEqual( + AppConfig.baseURL(for: .staging), + AppConfig.baseURL(for: .production), + "Staging and production must not share base URL" + ) + } + + // MARK: - supabaseURL per environment + + func testLocalSupabaseURLPointsToLocalhost() { + let url = AppConfig.supabaseURL(for: .local) + XCTAssertEqual(url.host, "127.0.0.1") + } + + func testStagingSupabaseURLPointsToStagingProjectRef() { + let url = AppConfig.supabaseURL(for: .staging) + XCTAssertTrue( + url.absoluteString.contains("zzdamokfeenencuggjjp"), + "Expected staging project ref zzdamokfeenencuggjjp in \(url)" + ) + } + + func testProductionSupabaseURLPointsToProdProjectRef() { + let url = AppConfig.supabaseURL(for: .production) + XCTAssertTrue( + url.absoluteString.contains("xybyzflfxnqqyxnvjklv"), + "Expected prod project ref xybyzflfxnqqyxnvjklv in \(url)" + ) + } + + func testStagingAndProductionSupabaseURLAreDistinct() { + XCTAssertNotEqual( + AppConfig.supabaseURL(for: .staging), + AppConfig.supabaseURL(for: .production), + "Staging and production must point at different Supabase projects" + ) + } + + // MARK: - supabaseAnonKey per environment + + func testStagingAnonKeyIsValidJWT() { + let key = AppConfig.supabaseAnonKey(for: .staging) + XCTAssertTrue(key.hasPrefix("eyJhbGc"), "Expected JWT prefix") + XCTAssertEqual(key.split(separator: ".").count, 3, "JWT must have 3 segments") + } + + func testProductionAnonKeyIsValidJWT() { + let key = AppConfig.supabaseAnonKey(for: .production) + XCTAssertTrue(key.hasPrefix("eyJhbGc"), "Expected JWT prefix") + XCTAssertEqual(key.split(separator: ".").count, 3, "JWT must have 3 segments") + } + + func testStagingAndProductionAnonKeysAreDistinct() { + XCTAssertNotEqual( + AppConfig.supabaseAnonKey(for: .staging), + AppConfig.supabaseAnonKey(for: .production), + "Staging and production must have different anon keys" + ) + } + + // MARK: - Backwards compatibility + + func testCurrentEnvironmentResolvesViaEnvironmentInjection() { + // baseURL property must still resolve to baseURL(for: .current) + XCTAssertEqual(AppConfig.baseURL, AppConfig.baseURL(for: AppEnvironment.current)) + } +} From c9507c4427432e233151f11ae290ad20cc3925c3 Mon Sep 17 00:00:00 2001 From: Johan Lindengard Date: Thu, 7 May 2026 15:47:34 +0200 Subject: [PATCH 2/2] docs(operations): document demo parity audit results Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/operations/demo-parity-local-staging.md | 28 +- docs/operations/ios-demo-parity-audit.md | 368 +++++++++++++++++++ 2 files changed, 386 insertions(+), 10 deletions(-) create mode 100644 docs/operations/ios-demo-parity-audit.md diff --git a/docs/operations/demo-parity-local-staging.md b/docs/operations/demo-parity-local-staging.md index dca5dbc2..a35d2252 100644 --- a/docs/operations/demo-parity-local-staging.md +++ b/docs/operations/demo-parity-local-staging.md @@ -3,7 +3,7 @@ title: "Demo Parity — Local vs Staging" description: "Audit av skillnader mellan lokal demo och staging-demo. Staging är inte demo-redo idag: demo_mode inaktivt, demo-seed inte körd, Test Testsson läcker. Konkret minsta åtgärd dokumenterad. Inkluderar Erik Järnfot vs Maria Lindgren persona-bedömning." category: operations status: active -last_updated: 2026-05-06 +last_updated: 2026-05-07 tags: [demo-mode, staging, parity, audit, demo-readiness, demo-persona] related: - ../demo-mode.md @@ -442,15 +442,23 @@ Efter slicen utförd: --- -## STOPP — inväntar Johan innan staging ändras +## Utfall (2026-05-07) -**Inga writes mot staging. Inga commits. Inga env-ändringar utförda.** +| Steg | Status | Detalj | +|------|--------|--------| +| Steg A — sätt demo_mode env via REST API | KLAR | `NEXT_PUBLIC_DEMO_MODE=true` + `DEMO_MODE_SEED_FALLBACK=true` på Preview/staging via REST API DELETE+POST. Redeploy commit `e904eb97`. | +| Steg B — seed staging-DB med Erik Järnfot | KLAR | `seed-demo-provider.ts --reset` mot staging-pooler. Erik + 5 tjänster + 9 kunder + 14 hästar + 18 bokningar + 7 recensioner + 1 bokningsserie + Smart Reply-konversation. | +| Steg C — redeploy för att aktivera flag | KLAR | Kombinerades med Steg A:s redeploy (commit `e904eb97`). | +| Steg D — manuell verifiering via DemoLoginButton | KLAR | Johan verifierade browser-baserat 2026-05-07. Demo-login fungerar mot staging. | -Säg: -- **"kör Steg A — sätt demo_mode env via REST API"** — ~5 min, ingen DB-impact -- **"kör Steg B — seed staging-DB"** — kräver att du först exportera STAGING_POOLER_URL via `read -rsp`-kommando -- **"kör hela A→D"** — 30 min totalt, du verifierar efter Steg D i browser -- **"vänta, fråga om X"** — annan riktning -- **"committa rapporten själv"** — på `staging`-branch utan att utföra slicen +### Gotcha upptäckt under Steg B -Min rekommendation: **committa rapporten + kör Steg A**. Steg A är låg-risk och bevisar REST API-vägen igen efter dagens incident. +`supabase/.temp/pooler-url` (cachad av `supabase link`) **saknar password helt** — formatet är `postgresql://user@host:port/db` utan `:password@`-segmentet. `supabase`-CLI läser password från egen credential-store under runtime, men direkt-Prisma/`pg`-anrop får tom string och failar med `P1000: Authentication failed`. **Lösning:** vid manuell DB-access mot staging — bygg URL från lösenordshanterare och paste:a via tyst prompt (`read -rsp`) till `.env.local` (variabelnamn: `STAGING_POOLER_URL`). Inte återanvänd `.temp/pooler-url`. + +### Eftersläp + +| Vad | Hantering | +|-----|-----------| +| `Test Testsson` finns kvar i staging | Inte demo-blocker. Kan rensas via SQL om det stör i nästa demo. | +| Maria Lindgren rebrand till `maria.lindgren@demo.equinet.se` | Optional, inte gjort. Maria är teknisk fixture, inte demo-persona. | +| Pre-build-guard som rejecter tomma critical env vars | Föreslagen efter S64-4 + 2026-05-06-incident, inte byggd. Separat slice. | diff --git a/docs/operations/ios-demo-parity-audit.md b/docs/operations/ios-demo-parity-audit.md new file mode 100644 index 00000000..44547b89 --- /dev/null +++ b/docs/operations/ios-demo-parity-audit.md @@ -0,0 +1,368 @@ +--- +title: "iOS Demo Parity Audit" +description: "Audit av iOS-app:ens stöd för demo-mode jämfört med local web och staging web. Identifierar tre kritiska gap: AppConfig-URL:er pekar på döda/fel domäner, ingen DemoLoginButton i native UI, ingen Vercel share-link-hantering. Read-only audit, inga ändringar utförda." +category: operations +status: active +last_updated: 2026-05-07 +tags: [ios, demo-mode, parity, audit, staging, vercel, demo-readiness] +related: + - demo-parity-local-staging.md + - demo-mode.md + - environments.md + - staging-environment-setup.md + - url-configuration.md +sections: + - Sammanfattning + - 1. Current iOS architecture + - 2. Environment config — URL-matris + - 3. Auth/login model + - 4. Demo mode support + - 5. Local / Staging / Prod comparison + - 6. Risker + - 7. Recommended next slices + - 8. Do-not-do-lista + - STOPP — inväntar Johan +--- + +# iOS Demo Parity Audit (2026-05-07) + +> **Read-only audit.** Inga kod-, Xcode-, env-, Vercel-, eller Supabase-ändringar utförda. + +--- + +## Sammanfattning + +iOS-appen har grundläggande demo-mode-stöd via feature flag `demo_mode` från `/api/feature-flags`. Däremot finns **tre kritiska gap** som blockerar att iOS kan demo:as mot staging idag: + +| # | Gap | Konsekvens | Refererad fil | +|---|-----|------------|---------------| +| **1** | `AppConfig.staging` pekar på **död URL** `https://equinet-git-staging-cola500.vercel.app` (returnerar 404 — gammal cola500-org-URL, inte längre aktiv) | iOS kan inte nå staging alls i `-STAGING`-läge | `ios/Equinet/Equinet/AppConfig.swift:47` | +| **2** | `AppConfig.production` pekar på `https://equinet-app.vercel.app` istället för custom domain `https://equinet.johanlindengard.com` | Fungerar idag (Vercel-default-domain returnerar 429 firewall, inte 401 SSO), men inte den dokumenterade prod-URL:en | `ios/Equinet/Equinet/AppConfig.swift:49` | +| **3** | Ingen Vercel share-link-hantering i WebView (`_vercel_share` query, `_vercel_jwt` cookie) | iOS kan **inte** öppnas via en share-länk om staging är SSO-skyddad. WebView 401-handlern triggar logout vid SSO-challenge | `ios/Equinet/Equinet/WebView.swift:392-400` | + +Sekundärt: Ingen `DemoLoginButton` i native UI (Demo-login är bara i webben). iOS-användare kan inte trigga demo-konto med ett klick i native-vy. + +--- + +## 1. Current iOS architecture + +iOS-appen är en hybrid-app: native SwiftUI-skärmar (Native*View) drivs av API-anrop till `/api/native/*`, och övriga vyer (Mer-meny → Meddelanden, Hjälp, etc.) embeddar webbsidor i WKWebView. + +**Huvudkomponenter:** + +- **`AppConfig.swift`** — central environment-konfiguration. Three cases: `.local`, `.staging`, `.production`. Switching via `-STAGING` launch arg (DEBUG only) eller automatiskt `.production` i Release-build. +- **`SupabaseManager`** — Singleton wrapping Supabase Swift SDK. Auth tokens persisterade i App Group Keychain (`group.com.equinet.shared`). +- **`AuthManager`** — Native login via Supabase Swift SDK. Efter inloggning: `exchangeSessionForWebCookies()` POST till `/api/auth/native-session-exchange` med Bearer token → får `Set-Cookie`-svar → cookies lagras i `HTTPCookieStorage.shared` → injiceras i WKWebView för WebView-sessioner. +- **`APIClient.swift`** — REST-klient för `/api/native/*` endpoints. `Authorization: Bearer ` header. 15s timeout, 401 → single refresh + retry, 429 → throw med `Retry-After`. +- **`WebView.swift`** — WKWebView-wrapper. Lyssnar på Supabase `authStateChanges` för `tokenRefreshed`-event och re-exchange:ar cookies. CSS-injektion döljer webb-chrome (BottomTab + Header) för provider-vyer. +- **`BridgeHandler.swift`** — JS↔Swift-kommunikation: `equinet.postMessage(...)` (JS → Swift) och `equinetNative.onMessage(...)` (Swift → JS). 17 meddelandetyper inkl. push, network, calendar sync, speech recognition, navigation. +- **`AppCoordinator`** — Tab-state, feature-flag-cache (UserDefaults, fetched i bakgrunden från `/api/feature-flags`). + +**Native screens med direkt API-anrop:** + +| Vy | ViewModel | Endpoint | +|---|---|---| +| NativeDashboardView | DashboardViewModel | `/api/native/dashboard` | +| NativeCalendarView | CalendarViewModel | `/api/native/calendar` | +| NativeBookingsView | BookingsViewModel | `/api/native/bookings`, `/api/bookings/{id}` | +| NativeServicesView | ServicesViewModel | `/api/native/services` | +| NativeCustomersView | CustomersViewModel | `/api/native/customers/*` | +| NativeProfileView | ProfileViewModel | `/api/native/provider/profile` | +| NativeReviewsView | ReviewsViewModel | `/api/native/reviews` | +| NativeDueForServiceView | DueForServiceViewModel | `/api/native/due-for-service` | +| NativeAnnouncementsView | AnnouncementsViewModel | `/api/native/announcements` | +| NativeInsightsView | InsightsViewModel | `/api/native/insights` | +| MoreWebView (Meddelanden, Hjälp, etc.) | — | Web paths (`/provider/*`) — använder cookies efter exchange | + +**Build-konfiguration:** + +- 2 schemes: `Equinet` + `EquinetWidgetExtension`. Inga separata Staging-schemes. +- 2 configurations: Debug + Release. Inga separata xcconfig-filer. +- `-STAGING` launch arg är ej fördefinierad i `Equinet.xcscheme` — användaren måste manuellt redigera schemen i Xcode (Product → Scheme → Edit → Run → Arguments). + +--- + +## 2. Environment config — URL-matris + +### Vad iOS faktiskt pekar på idag + +| Environment | iOS `AppConfig.baseURL` | Faktisk live-URL för 2026-05-07 | Status | +|---|---|---|---| +| `.local` | `http://localhost:3000` | localhost:3000 | OK om `npm run dev` körs | +| `.staging` | `https://equinet-git-staging-cola500.vercel.app` | **HTTP 404** (org `cola500` finns inte längre, vi flyttade till `cola500s-projects`) | **BRUTEN** | +| `.production` | `https://equinet-app.vercel.app` | HTTP 429 (firewall) — fungerar för riktiga klienter | Fungerar men inte dokumenterad prod-URL | + +### Vad iOS borde peka på + +| Environment | Korrekt URL | Verifiering | +|---|---|---| +| `.local` | `http://localhost:3000` | OK — ingen ändring | +| `.staging` | `https://equinet-staging.johanlindengard.com` | HTTP 401 (Vercel SSO) — kräver share-link-flöde eller annan bypass | +| `.production` | `https://equinet.johanlindengard.com` | HTTP 429 (firewall) — fungerar | + +### Supabase-konfiguration + +| Environment | iOS `AppConfig.supabaseURL` | Status | +|---|---|---| +| `.local` | `http://127.0.0.1:54321` | OK (Supabase CLI) | +| `.staging` | `https://zzdamokfeenencuggjjp.supabase.co` | **OK** — pekar på samma staging Supabase som webb | +| `.production` | `https://zzdamokfeenencuggjjp.supabase.co` (via fallthrough till staging) | **FEL** — kommentar säger "until separate prod project". Detta var medvetet före Apple Developer Program-uppgradering, men nu finns separat prod-Supabase `xybyzflfxnqqyxnvjklv` | + +> **Notera:** iOS `.production` använder staging Supabase! Det betyder iOS-prod-användare loggar in mot **staging-DB**, inte prod-DB. Detta är dokumenterat som tillfälligt i kodkommentar (`AppConfig.swift:73`), men betyder att iOS i nuläget inte är produktionsskarp. + +--- + +## 3. Auth/login model + +### Native login (default) + +``` +User → NativeLoginView (TextField + SecureField) + → AuthManager.login(email:password:) + → SupabaseManager.client.auth.signIn(email:password:) + → Session lagras i App Group Keychain + → exchangeSessionForWebCookies() POST /api/auth/native-session-exchange + → Set-Cookie response → HTTPCookieStorage.shared + → WebView konfigureras med cookies + laddas +``` + +**Filer:** +- `NativeLoginView.swift` — UI +- `AuthManager.swift:115-146` — `login()` +- `AuthManager.swift:226-265` — `exchangeSessionForWebCookies()` +- `AuthManager.swift:299-310` — `buildExchangeRequest()` + +### Native API-anrop + +``` +APIClient.fetchDashboard() + → URL: AppConfig.baseURL + "/api/native/dashboard" + → Header: Authorization: Bearer + → 15s timeout + → 401 → refresh session, retry once → om misslyckas: throw APIError.unauthorized + → 429 → throw APIError.rateLimited(retryAfter:) +``` + +### WebView-anrop + +``` +WebView laddar URL + → WKWebView läser HTTPCookieStorage.shared + → Skickar cookies som vanlig browser + → 401-svar i WebView → AuthManager.logout() (omedelbart) +``` + +**Refresh-loop:** +- `WebView.swift:268-284` — observer på `SupabaseManager.client.auth.authStateChanges` +- Vid `tokenRefreshed`-event → re-exchange → cookies uppdaterade + +### Token-flöde till `/api/native/*` + +iOS skickar **Bearer JWT**, inte cookies. Skillnad från webb-frontend som använder cookies via Next.js Server Components. + +``` +iOS APIClient → /api/native/dashboard + Authorization: Bearer eyJhbGc... + (no Cookie header) +``` + +### MobileToken-flöde (DEAD CODE) + +`KeychainHelper.swift:119-151` har `saveMobileToken()`, `loadMobileToken()`, `clearMobileToken()` — men **anropas aldrig**. Lagrad infrastruktur för en framtida flow där iOS skulle byta Supabase-token mot en längre-livad MobileToken JWT (90d). Inte aktiv idag. + +--- + +## 4. Demo mode support + +### Webb (local + staging) + +| Komponent | Plats | Funktion | +|---|---|---| +| `NEXT_PUBLIC_DEMO_MODE` env var | Vercel Preview | Sätter `isDemoMode()` runtime | +| `DemoLoginButton.tsx` | `src/components/landing/` | Hardcoded `erik.jarnfot@demo.equinet.se` / `DemoProvider123!`, syns på landing om demo-mode aktivt | +| Demo-tabs filter | Provider-layout | Visar bara `Tjänster` + `Profil` i demo-läge | +| Demo-data | Erik Järnfot seedad i staging-DB | 18 bokningar, 9 kunder, 14 hästar, 1 serie, Smart Reply-konversation | + +### iOS + +| Komponent | Plats | Funktion | +|---|---|---| +| Feature flag `demo_mode` | `/api/feature-flags` (public endpoint) | Cached i UserDefaults via `AppCoordinator:65-81` | +| `NativeMoreView` filter | `NativeMoreView.swift:73-82` | Om `demo_mode=true`: bara `/provider/services` + `/provider/profile` syns i Mer-menyn (speglar webb-demoTabs) | +| `NativeProfileView` filter | `NativeProfileView.swift:20, 118, 332-345` | `isDemoMode` döljer länkar-sektion, self-reschedule, recurring bookings, men VISAR booking settings + availability | +| **Demo-login-knapp i native UI** | — | **SAKNAS** | +| **Hardcoded demo-konto i iOS** | — | **SAKNAS** | +| **Build-flagga / scheme för demo** | — | **SAKNAS** | +| **`NEXT_PUBLIC_DEMO_MODE`-läsning i iOS** | — | Ej relevant — iOS läser bara feature flag från servern | + +**Praktisk konsekvens:** En användare som öppnar iOS-appen mot staging behöver fortfarande logga in manuellt med `erik.jarnfot@demo.equinet.se` / `DemoProvider123!` via NativeLoginView. Det finns ingen "Demo-login"-knapp att trycka på. + +--- + +## 5. Local / Staging / Prod comparison + +| Aspekt | Local web | Staging web | iOS local | iOS staging | iOS production | +|---|---|---|---|---|---| +| **Base URL** | `localhost:3000` | `equinet-staging.johanlindengard.com` (custom) | `localhost:3000` | `equinet-git-staging-cola500.vercel.app` (**404**) | `equinet-app.vercel.app` (Vercel-default) | +| **Auth-mekanism** | Supabase cookies | Supabase cookies | Supabase Swift SDK + cookie-exchange | Supabase Swift SDK + cookie-exchange | Supabase Swift SDK + cookie-exchange | +| **Demo-login (1-klick)** | Ja (DemoLoginButton) | Ja (DemoLoginButton) | Nej (manuell login) | Nej (manuell login) | — | +| **Demo-data** | Lokal seed (Maria-fixture + Anna-testdata) | Erik Järnfot (staging-DB) | Lokal seed | Erik Järnfot (om iOS pekade rätt) | Staging-DB (FEL — iOS prod använder staging Supabase) | +| **Feature flag `demo_mode`** | Toggleable via admin | Aktiv (`NEXT_PUBLIC_DEMO_MODE=true`) | Toggleable | Aktiv | Toggleable | +| **Vercel Authentication** | N/A | SSO 401 (custom domain mappat till preview) | N/A | Skulle få SSO 401 om iOS pekade rätt | Fungerar (Vercel-default-domain) | +| **Vercel share-link-stöd i klient** | N/A | Ja (browser hanterar `_vercel_share` automatiskt) | N/A | **Nej** (WebView ignorerar query-param) | N/A | +| **API endpoints** | `/api/*` | `/api/*` | `/api/native/*` (Bearer) + `/api/*` (cookie) | Samma — om URL fungerade | Samma | +| **iOS bridge** | N/A | N/A | JS↔Swift via `webkit.messageHandlers.equinet` | Samma | Samma | +| **Test Testsson finns kvar** | Beror på lokal DB | Ja (oren staging-DB) | Beror på lokal DB | Ja (om iOS når staging) | I staging-DB pga (5) | + +### Known gaps i jämförelsetabell + +- iOS staging-URL är **bruten** (404) +- iOS prod-URL är **inte den dokumenterade prod-URL:en** +- iOS prod **använder staging Supabase** (medvetet, men inte produktionsskarpt) +- iOS har **ingen 1-klicks demo-login** +- iOS WebView **kan inte hantera Vercel share-link** för SSO-bypass + +--- + +## 6. Risker + +### 6.1 iOS staging är otestbart idag + +**Risk:** Den hardkodade staging-URL:en (`equinet-git-staging-cola500.vercel.app`) returnerar 404 — ingen kan sätta `-STAGING`-arg i Xcode och testa appen mot staging utan att först fixa AppConfig. + +**Påverkan:** All iOS staging-verifiering blockerad. Nya staging-features kan inte verifieras innan deploy till prod. + +**Sannolikhet:** Hög — händer vid första försöket att starta iOS i staging-läge. + +### 6.2 iOS production använder staging Supabase + +**Risk:** `AppConfig.swift:73` säger explicit "Both [staging+production] use staging project (zzdamokfeenencuggjjp) until Apple Developer Program is purchased and separate prod bundle ID + project is created." + +**Påverkan:** iOS-prod-användare loggar in mot staging-DB. All iOS-data hamnar i staging-DB. Prod-Supabase (`xybyzflfxnqqyxnvjklv`) får aldrig iOS-trafik. + +**Sannolikhet:** Hög — sker 100% av iOS-prod-anrop idag. Inte ett "fel" eftersom det är dokumenterat och medvetet, men måste fixas innan iOS-app släpps publikt. + +### 6.3 iOS WebView fastnar bakom Vercel SSO på staging + +**Risk:** Om AppConfig.staging fixas till `equinet-staging.johanlindengard.com`, träffar iOS WebView Vercel SSO-401. WebView.swift:392-400 triggar omedelbart logout vid 401 — detta kommer att se ut som att appen "tappat sessionen" trots att Supabase-tokenen är giltig. + +**Påverkan:** iOS staging-läge funktionellt oanvändbart utan SSO-bypass. Native API-anrop (Bearer) kan också få 401 från Vercel SSO innan de når app-lagret — APIClient kan inte skilja mellan "Vercel-SSO-401" och "Supabase-401" och kommer förgäves försöka refresh:a Supabase-tokenen. + +**Sannolikhet:** Hög — händer 100% av staging-anrop utan bypass-cookie eller Deployment Protection Exception. + +### 6.4 Vercel share-link-cookien följer inte med iOS WebView + +**Risk:** `_vercel_jwt`-cookie sätts av Vercel som `HttpOnly; Secure; SameSite=Lax`. WKWebView kan ta emot cookien om användaren först klickar share-länken i Safari och sen öppnar appen — men de delar **inte** cookies. Default WKWebView har egen `WKWebsiteDataStore` som inte läser från Safari. + +**Påverkan:** Även om man klickar share-länk i Safari före iOS-appen → cookien gäller inte i appen. Ingen enkel "öppna staging i iOS via share-link"-flöde. + +**Sannolikhet:** Hög — naturlig användarförväntan ("jag fick länken, jag öppnar appen") fungerar inte. + +### 6.5 Demo-login saknas i iOS + +**Risk:** Användare som vill testa iOS mot demo-data behöver känna till Erik:s mejl + lösenord och skriva in dem manuellt i NativeLoginView. För investorer eller tidiga testare är det friktion. + +**Påverkan:** iOS-demo har sämre user experience än webb-demo. Kan misstolkas som "iOS är inte färdigt" trots att funktionaliteten finns. + +**Sannolikhet:** Medel — bara ett problem om iOS-demos visas externt. + +### 6.6 Native API auth kan inte använda Vercel bypass-cookie + +**Risk:** Vercel `_vercel_jwt`-bypass-cookie räknas bara för `Cookie:`-header-anrop, inte `Authorization: Bearer`. iOS APIClient skickar bara Bearer — alltså kan share-link aldrig hjälpa native API-anrop att passera SSO. + +**Påverkan:** Även om WebView bypass:as via cookie, så får native API-anrop fortfarande 401 från Vercel SSO. iOS skulle behöva separat lösning för native API. + +**Sannolikhet:** Hög — strukturell gränssättning. + +### 6.7 iOS native staging-test riskerar att skicka data till prod-Supabase i framtiden + +**Risk:** Om någon "fixar" `AppConfig.swift:73` till `xybyzflfxnqqyxnvjklv` (prod) utan att samtidigt skapa en separat staging-Supabase-klient, kommer iOS staging-läge att skriva till prod-DB. + +**Påverkan:** Datacorruption i prod om bara webb-baseURL fixas och inte Supabase-URL. + +**Sannolikhet:** Medel — möjligt vid hastig "fix everything"-PR. + +--- + +## 7. Recommended next slices + +Tre alternativ, prioriterade efter värde/risk-förhållande: + +### Slice A — Fix AppConfig URL:er (1-2 timmars arbete, låg risk) + +**Vad:** Uppdatera `AppConfig.swift` så `.staging` → `equinet-staging.johanlindengard.com` och `.production` → `equinet.johanlindengard.com`. Lägg till separat prod-Supabase-URL (`xybyzflfxnqqyxnvjklv`) för `.production`. + +**Tester:** Existing XCTest för auth + APIClient bör fortsätta vara gröna (de använder mock URLSession). Manuell test krävs för att bekräfta att iOS i `-STAGING`-läge träffar nya domänen. + +**Värde:** Bara docs-koherens. Funktionellt blockeras fortfarande iOS staging av Vercel SSO efter URL-fix. + +**Risk:** Låg. Behöver verifiera att prod-iOS faktiskt fungerar mot custom domain (idag fungerar `equinet-app.vercel.app` mot prod-deployment, custom domain gör samma men officiellt). + +**Begränsning:** Ensam slice räcker INTE för demo-mot-staging — kräver också Slice B eller annan SSO-bypass. + +### Slice B — iOS staging Deployment Protection-bypass (komplex, hög risk) + +**Vad:** Bygg en mekanism i `WebView.swift` + `APIClient.swift` som läser en lokal `STAGING_VERCEL_BYPASS_TOKEN` från Info.plist eller en debug-only källa, och skickar `?x-vercel-protection-bypass=` på första request samt mottar `_vercel_jwt`-cookie för efterföljande WebView-anrop. För native API-anrop: skicka token som query-param på varje request (eftersom Bearer-anrop inte har cookies). + +**Tester:** Nya XCTest för bypass-token-injektion. Manuell verifiering att staging fungerar med token, blockeras utan. + +**Värde:** Möjliggör iOS staging-test för utvecklare/QA. Skulle inte distribueras till slutanvändare (DEBUG-only). + +**Risk:** Hög komplexitet. Token måste lagras säkert (Info.plist är inte säkert — inkluderas i app-bundle synligt). Bypass-tokens roteras periodiskt → kräver rebuild varje gång. Inte ideal lösning. + +**Alternativ:** Skapa ett separat staging-Vercel-projekt enligt audit-rekommendationen i `demo-parity-local-staging.md` så att custom-domain-undantaget gäller iOS staging-domain → ingen bypass behövs. + +### Slice C — Native DemoLoginButton + dokumentera iOS-mot-staging-flöde (medel arbete, låg risk) + +**Vad:** Lägg till en `DemoLoginButton`-equivalent i `NativeLoginView` som visas när feature flag `demo_mode=true`. Auto-fyller email + lösenord och kallar `AuthManager.login()`. Plus: skapa körinstruktion `docs/operations/ios-staging-demo.md` som beskriver hur QA testar iOS mot staging idag. + +**Tester:** XCTest för NativeLoginView + AuthManager.login med demo-konto. + +**Värde:** Minskar friktion för iOS-demo. Pre-requirement: Slice A + (B eller separat staging-projekt) måste fixas innan demo-login fungerar mot staging. + +**Risk:** Låg. Hardcoded-konto är samma säkerhetsrisk som i webben — Erik:s konto är public knowledge i staging. + +### Sammansatt rekommendation + +**Bästa väg om iOS-demo blir prio:** Slice A → separat staging-Vercel-projekt enligt webb-audit (`demo-parity-local-staging.md`) → Slice C. Tre slices, ~1-2 dagar totalt. + +**Bästa väg om iOS-demo INTE är prio:** Notera gap i backlog, prioritera webb-demo. iOS prod-användning fungerar fortfarande (mot staging Supabase) — inte demo-blocker. + +**Om iOS staging-test blir akut:** Slice A + Slice B (DEBUG-only bypass). Minst snyggt men snabbast. + +--- + +## 8. Do-not-do-lista + +| Vad | Varför | +|-----|--------| +| Fixa bara `AppConfig.production`-URL utan att överväga Supabase-URL | iOS prod använder staging Supabase idag (`AppConfig.swift:73`). Att bara byta webb-URL:en utan att skapa separat prod-Supabase-klient lämnar inkonsistent state. | +| Lägg till `_vercel_share`-cookie-hantering i WKWebView genom att hardcoda token | Bypass-tokens roteras → token i kodbasen blir snabbt inaktuell. Plus säkerhetsrisk om bundle reverse-engineeras. | +| Bygg native demo-login som hardcodar Erik:s mejl + lösenord i Release-build | Production-app ska inte ha demo-konton kompilerade in. Använd `#if DEBUG` eller feature flag-driven så de bara syns i staging. | +| Migrera iOS från Bearer till cookie-auth för `/api/native/*` för att kunna använda share-cookie | Bearer JWT är arkitekturellt rätt för native (separat session-livscykel). Cookie-auth öppnar CORS- och CSRF-frågor som inte finns idag. | +| Försök "tappa bort" iOS staging tills separate staging-projekt skapas | Brukrer som idag pekar på död URL får inget felmeddelande, bara nätverkstimeout. Bättre att fixa AppConfig-URL:en även om SSO fortfarande blockerar. | +| Aktivera iOS-bygge mot prod-Supabase utan separat bundle ID | Apple App Store-process kräver att en specifik bundle ID identifierar appen. Att byta Supabase-URL utan bundle-strategi skapar konflikt med framtida App Store Connect-setup. | +| Bygg en "demo användare slumpas" mekanism i iOS | Staging har bara en demo-persona (Erik). Skulle kräva fler personor i seed + UI-val för att slumpa, vilket är out-of-scope för demo-parity-slice. | +| Köra `seed-demo-provider.ts --reset` mot prod-Supabase | Aldrig. Prod-DB har riktig data. | + +--- + +## STOPP — inväntar Johan innan någon iOS-ändring + +Audit klar. Inga ändringar utförda i: + +- iOS Swift-kod (AppConfig, AuthManager, WebView, APIClient, etc.) +- Xcode-projekt (schemes, configurations, xcconfig) +- Vercel (deployment protection, exceptions, env vars) +- Supabase (env, RLS, demo-data) +- Webb (DemoLoginButton, layout, middleware) +- Git (inga commits, inga pushes, working tree rent förutom denna nya rapport) + +Säg: +- **"plan A — fixa AppConfig URL:er"** — jag bryter ner Slice A i konkreta TDD-steg och skriver branch-prep +- **"plan A + nytt staging-Vercel-projekt"** — Slice A + planera separat staging-projekt enligt webb-audit +- **"plan C — DemoLoginButton native"** — bara native-demo-knapp, lämna URL/SSO för senare +- **"parkera, dokumentera bara"** — committa rapporten, ingen kod-ändring +- **"vänta, fundera"** — annan riktning + +Inväntar.