Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

302 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Wizper - Voice-First Dating Platform 🎙️

Wizper ist eine moderne, sprachbasierte Dating-Plattform, die authentische Verbindungen durch Voice-First-Kommunikation ermöglicht.

📋 Inhaltsverzeichnis

  1. Architektur-Überblick
  2. Projektstruktur
  3. Technologie-Stack
  4. Komplexe Systeme im Detail
  5. Lokales Setup
  6. Umgebungsvariablen
  7. Deployment
  8. Troubleshooting

🚦 Onboarding Flow (Kurz)

  • Welcome → Location → Firstname → Birthday → Profile: Gender → Interested In → Orientation → Looking For → Passions → Age Verification → Voice → Subscription (weekly/monthly)

Stripe Subscription Flow (Kurz)

  • Setup Intent: Customer erstellen + client_secret
  • Payment Method bestätigen (Card)
  • Payment Method an Customer anhängen, als Default setzen
  • Subscription erstellen mit passendem priceId (weekly/monthly)

🔌 API Endpoints (Kurz)

  • POST /api/create-setup-intent
    • Erstellt Stripe Customer + SetupIntent
    • Response: clientSecret, customerId
  • POST /api/create-subscription
    • Body: { customerId, paymentMethodId, priceId, email?, name? }
    • Erstellt Subscription mit übergebenem priceId
  • POST /api/cancel-subscription
    • Body: { userId }
  • POST /api/reactivate-subscription
    • Body: { userId }
  • POST /api/create-checkout-session
    • Body: { userId, priceId }
    • Response: { sessionId }
  • POST /api/get-user-interests
    • Body: { userIds: string[] }
    • Response: { items: { user_id, interest_id }[] }

Hinweise:

  • JSON senden, kein GET für /api/get-user-interests (GET antwortet mit "Cannot GET").
  • CORS erlaubt: https://wizper.cloud, https://www.wizper.cloud, http://localhost:5173, http://localhost:3000.

🏗️ Architektur-Überblick

Das Projekt ist als Monorepo aufgebaut und besteht aus mehreren Hauptkomponenten:

┌─────────────────────────────────────────────────────────────┐
│                     Wizper Platform                          │
├─────────────────────────────────────────────────────────────┤
│                                                               │
│  ┌──────────────┐   ┌──────────────┐   ┌──────────────┐    │
│  │   Frontend   │   │   API Server │   │   Signaling  │    │
│  │  React + TS  │◄─►│   Express    │◄─►│    Server    │    │
│  │   Capacitor  │   │   Node.js    │   │   WebSocket  │    │
│  └──────────────┘   └──────────────┘   └──────────────┘    │
│         │                   │                   │            │
│         ▼                   ▼                   ▼            │
│  ┌──────────────────────────────────────────────────────┐   │
│  │           External Services & Infrastructure          │   │
│  ├──────────────────────────────────────────────────────┤   │
│  │  • Supabase (Database, Auth, Storage)                │   │
│  │  • Stripe (Payments, Age Verification)               │   │
│  │  • Eigenes WebRTC (Signaling Server + TURN/Coturn)   │   │
│  │  • Nominatim (Reverse Geocoding)                     │   │
│  └──────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘

Komponenten

🎨 Frontend

  • Framework: React 19 + TypeScript + Vite
  • Mobile: Capacitor für iOS & Android
  • State Management: Zustand
  • Styling: Styled-components
  • Routing: React Router v7

🔧 Backend (API Server)

  • Framework: Express.js + TypeScript
  • Hauptaufgaben:
    • Stripe Payment Processing
    • Age Verification
    • Subscription Management
    • Webhook Handling

📡 Signaling Server

  • Framework: Node.js + Socket.io
  • Zweck: WebRTC Signaling für Voice Calls

🗄️ Supabase (BaaS)

  • PostgreSQL Database
  • Authentication (Email, OAuth)
  • Row Level Security (RLS)
  • Storage (Voice samples, Profile pictures)
  • Realtime (Presence, Messages)

📁 Projektstruktur

wizper/
├── src/                          # Frontend Source Code
│   ├── app/                      # App-Level (Router, Providers)
│   ├── features/                 # Feature-based Organization
│   │   ├── auth/                 # Authentication (Login, Register, OAuth)
│   │   ├── onboarding/           # Multi-Step Onboarding
│   │   │   ├── components/
│   │   │   │   ├── steps/        # Individual Onboarding Steps
│   │   │   │   ├── AgeVerificationStep.tsx
│   │   │   │   └── SubscriptionStep.tsx
│   │   │   └── pages/
│   │   │       └── OnboardingPage.tsx
│   │   ├── calling/              # Voice Calling System
│   │   │   ├── components/       # Call UI Components
│   │   │   └── services/         # CallService, RingtoneService
│   │   ├── discover/             # User Discovery
│   │   ├── profile/              # User Profile Management
│   │   └── admin/                # Admin Dashboard
│   ├── shared/                   # Shared Resources
│   │   ├── components/           # Reusable UI Components
│   │   ├── lib/                  # Utilities (Supabase client)
│   │   ├── stores/               # Zustand Stores (auth, call, presence)
│   │   └── styles/               # Global Styles & Theme
│   └── types/                    # TypeScript Type Definitions
│
├── api/                          # Backend API Server
│   └── src/
│       └── index.ts              # Express Server with Stripe Integration
│
├── backend/
│   └── signaling/                # WebRTC Signaling Server
│       └── src/
│           └── index.ts          # Socket.io Server
│
├── supabase/
│   └── migrations/               # Database Migrations
│       ├── 001_initial_schema.sql
│       ├── 002_add_profile_fields.sql
│       ├── 004_add_subscription_fields.sql
│       └── 008_update_intention_enum.sql
│
├── ios/                          # iOS Capacitor App
│   └── App/
│       └── App/
│           └── Info.plist        # iOS Permissions & Config
│
├── android/                      # Android Capacitor App
│   └── app/
│       └── src/
│           └── main/
│               └── AndroidManifest.xml  # Android Permissions
│
└── docker-compose.yml            # Local Development Setup

🛠️ Technologie-Stack

Frontend

  • React 19.2 - UI Library
  • TypeScript 5.9 - Type Safety
  • Vite 7 - Build Tool & Dev Server
  • Styled Components 6 - CSS-in-JS
  • React Router 7 - Routing
  • Zustand 5 - State Management
  • Capacitor 8 - Native Mobile Wrapper
  • Stripe React - Payment UI Components
  • Native WebRTC - Voice Calls (CallService + Signaling + TURN)
  • Material-UI - UI Components (Age Verification)

Backend

  • Node.js 22 - Runtime
  • Express - Web Framework
  • Stripe Node SDK - Payment Processing
  • Supabase JS Client - Database & Auth

Infrastructure

  • Supabase - BaaS (PostgreSQL, Auth, Storage, Realtime)
  • Stripe - Payment & Age Verification
  • Eigener TURN (Coturn) - NAT Traversal für WebRTC-Anrufe
  • OpenStreetMap Nominatim - Reverse Geocoding
  • Railway/Fly.io - Deployment Platform

🔐 Komplexe Systeme im Detail

Authentication & Authorization System

Das Auth-System basiert auf Supabase Auth und verwendet einen zentralen ProtectedRoute Component für Route Guards.

Architektur

┌─────────────────────────────────────────────────────────────┐
│                    Auth & Route Protection                   │
├─────────────────────────────────────────────────────────────┤
│                                                               │
│  User besucht Route                                          │
│         │                                                     │
│         ▼                                                     │
│  ┌──────────────┐                                            │
│  │   Router     │                                            │
│  │  (router.tsx)│                                            │
│  └──────┬───────┘                                            │
│         │                                                     │
│         │ Route geschützt?                                   │
│         ├───────────────┬─────────────────┐                 │
│         │               │                 │                  │
│         ▼               ▼                 ▼                  │
│    Public Route   ProtectedRoute   ProtectedRoute           │
│    (Landing,      (Onboarding)     (Discover, Profile)      │
│     Login,                          requireOnboarding=true   │
│     Register)                                                │
│         │               │                 │                  │
│         │               ▼                 ▼                  │
│         │      ┌─────────────────┐  ┌─────────────────┐    │
│         │      │  Auth Check:    │  │  Auth Check:    │    │
│         │      │  User logged    │  │  User logged    │    │
│         │      │  in?            │  │  in + Onboard.  │    │
│         │      └────┬────────────┘  │  complete?      │    │
│         │           │               └────┬────────────┘    │
│         │           ├─ Yes → Render      │                 │
│         │           │                    ├─ Yes → Render   │
│         │           └─ No → /login       └─ No → Redirect  │
│         │                                                    │
│         ▼                                                     │
│    Component Rendered                                        │
│                                                               │
└─────────────────────────────────────────────────────────────┘

Route-Hierarchie

Öffentliche Routen (Jeder kann zugreifen):

  • / - Landing Page (Register)
  • /login - Login Page
  • /register - Register Page
  • /verify - Email Verification
  • /auth/callback - OAuth Callback

Geschützte Routen (Login erforderlich):

  • /onboarding - Onboarding Flow (nur für eingeloggte User)

Voll geschützte Routen (Login + Onboarding abgeschlossen):

  • /discover - User Discovery & Calling
  • /profile - User Profile Management
  • /admin - Admin Dashboard (zusätzlich: is_admin Flag)

ProtectedRoute Component

Location: src/app/ProtectedRoute.tsx

interface ProtectedRouteProps {
  children: React.ReactNode;
  requireOnboarding?: boolean; // Default: false
}

// Verwendung im Router:
<Route 
  path="/onboarding" 
  element={
    <ProtectedRoute>
      <OnboardingPage />
    </ProtectedRoute>
  } 
/>

<Route 
  path="/discover" 
  element={
    <ProtectedRoute requireOnboarding={true}>
      <DiscoverPage />
    </ProtectedRoute>
  } 
/>

Funktionsweise:

  1. Prüft Auth-Status:

    • isLoading → Zeigt Loading Screen
    • !user → Redirect zu /login
  2. Prüft Onboarding (wenn requireOnboarding=true):

    • profile === null → Wartet auf Profile-Load
    • !profile.onboarding_complete → Redirect zu /onboarding
  3. Rendert Component:

    • Wenn alle Checks erfolgreich → Children werden gerendert

Auth Store (shared/stores/authStore.ts)

State:

interface AuthState {
  user: User | null;              // Supabase User Object
  session: Session | null;        // Supabase Session
  profile: UserProfile | null;    // Custom User Profile aus DB
  isLoading: boolean;             // Auth-Initialisierung läuft
  error: string | null;           // Auth-Fehler
}

Actions:

- initialize()              // Initialisiert Auth, lädt Session & Profile
- login(email, password)    // Email/Password Login
- register(email, password) // Email/Password Registrierung
- loginWithGoogle()         // Google OAuth
- loginWithApple()          // Apple OAuth
- logout()                  // Logout & Cleanup
- fetchProfile()            // Lädt User Profile aus DB

Auth-Flow - User Journey

1. Neuer User (Registrierung):

1. User besucht Landing Page (/)
   → Redirects zu /login (wenn nicht eingeloggt)

2. User geht zu /register
   → Gibt Email + Password ein
   → register() wird aufgerufen

3. Supabase erstellt Auth User
   → session wird gesetzt
   → user wird gesetzt
   → profile ist null (noch nicht erstellt)

4. Redirect zu /onboarding
   → ProtectedRoute prüft: user ✓, profile.onboarding_complete ✗
   → Onboarding wird gerendert

5. User durchläuft Onboarding
   → Profile wird in DB erstellt
   → onboarding_complete = true

6. Redirect zu /discover
   → ProtectedRoute prüft: user ✓, onboarding_complete ✓
   → Discover Page wird gerendert

2. Bestehender User (Login):

1. User besucht Landing Page (/)
   → Redirects zu /login

2. User gibt Email + Password ein
   → login() wird aufgerufen

3. Supabase verifiziert Credentials
   → session wird gesetzt
   → user wird gesetzt
   → fetchProfile() lädt profile aus DB

4. Redirect basierend auf profile.onboarding_complete:
   
   a) Onboarding nicht abgeschlossen:
      → Redirect zu /onboarding
      
   b) Onboarding abgeschlossen:
      → Redirect zu /discover

3. OAuth Login (Google/Apple):

1. User klickt "Sign in with Google"
   → loginWithGoogle() wird aufgerufen

2. Supabase initiiert OAuth Flow
   → Redirect zu Google Login
   → User authentifiziert sich

3. Google redirected zu /auth/callback
   → Supabase parsed Token aus URL
   → session + user werden gesetzt

4. AuthCallbackPage prüft Status:
   → Lädt profile aus DB
   → Redirect zu /onboarding oder /discover

Wichtige Implementation Details

1. Auth Initialization (providers.tsx):

function AuthInitializer({ children }) {
  const initialize = useAuthStore((state) => state.initialize);

  useEffect(() => {
    initialize(); // Lädt Session & Profile beim App-Start
  }, [initialize]);

  return <>{children}</>;
}

2. Auth State Listener:

// In authStore.ts - initialize()
supabase.auth.onAuthStateChange(async (event, session) => {
  if (event === 'SIGNED_OUT') {
    set({ session: null, user: null, profile: null });
    return;
  }

  set({ session, user: session?.user ?? null });

  if (session?.user) {
    await fetchProfile(); // Lädt Profile nach Login
  }
});

3. Profile-Check für Onboarding:

// In ProtectedRoute.tsx
if (requireOnboarding) {
  if (profile === null) {
    return; // Wartet auf Profile-Load
  }

  if (!profile.onboarding_complete) {
    navigate('/onboarding', { replace: true });
    return;
  }
}

Security Best Practices

1. Row Level Security (RLS) in Supabase:

Alle Tabellen haben RLS Policies:

-- users Tabelle: User kann nur eigenes Profil bearbeiten
CREATE POLICY "Users can update their own profile"
ON users FOR UPDATE
TO authenticated
USING (auth.uid() = id)
WITH CHECK (auth.uid() = id);

-- voice_samples: User kann nur eigene Samples hochladen
CREATE POLICY "Users can upload their own voice samples"
ON voice_samples FOR INSERT
TO authenticated
WITH CHECK (user_id = auth.uid());

2. Service Role Key nur im Backend:

// ❌ NIEMALS im Frontend:
const supabase = createClient(url, SERVICE_ROLE_KEY);

// ✅ Nur ANON_KEY im Frontend:
const supabase = createClient(url, ANON_KEY);

3. Token-Storage:

  • Tokens werden automatisch in localStorage gespeichert
  • Key: sb-<project-ref>-auth-token
  • Beim Logout werden alle Auth-Tokens gelöscht

Troubleshooting Auth Issues

"User not logged in" trotz Login:

  • ✅ Check: Browser localStorage löschen
  • ✅ Check: Supabase Session expired? (7 Tage default)
  • ✅ Check: Network Tab: Requests zu Supabase erfolgreich?

Onboarding Loop (User kommt nicht zu /discover):

  • ✅ Check: profile.onboarding_complete in DB = true?
  • ✅ Check: fetchProfile() wird nach Onboarding aufgerufen?
  • ✅ Check: ProtectedRoute erhält aktualisiertes profile?

ProtectedRoute rendert nicht:

  • ✅ Check: isLoading bleibt true? → Auth-Init hängt
  • ✅ Check: Browser Console für Errors
  • ✅ Check: Supabase URL & Keys korrekt?

OAuth Callback Error:

  • ✅ Check: Redirect URL in Supabase Dashboard konfiguriert?
  • ✅ Check: ${window.location.origin}/auth/callback
  • ✅ Check: Mobile: Capacitor Deep Link konfiguriert?

Payment System (Stripe)

Das Payment System basiert auf Stripe und bietet zwei Subscription-Modelle:

Architektur

┌─────────────────────────────────────────────────────────────┐
│                     Payment Flow                             │
├─────────────────────────────────────────────────────────────┤
│                                                               │
│  Age Verification (Setup Intent)                             │
│  ┌──────┐  1. Request    ┌──────────┐  2. Create  ┌──────┐ │
│  │ User │─────────────────►│ Backend  │────────────►│Stripe│ │
│  │      │◄─────────────────│   API    │◄────────────│      │ │
│  └──────┘  3. Client      └──────────┘  Customer+  └──────┘ │
│             Secret                       Setup Intent         │
│     │                                                          │
│     │ 4. Confirm Card (Browser)                              │
│     ▼                                                          │
│  ┌──────┐                                                     │
│  │Stripe│  5. Payment Method Created                         │
│  │  JS  │                                                     │
│  └──────┘                                                     │
│     │                                                          │
│     │ 6. Save to Supabase (stripe_customer_id, pm_id)       │
│     ▼                                                          │
│  ┌──────────┐                                                 │
│  │ Supabase │  users.stripe_customer_id = cus_xxx           │
│  │   DB     │                                                 │
│  └──────────┘                                                 │
│                                                               │
│  Subscription (Payment Intent)                               │
│  ┌──────┐  7. Subscribe   ┌──────────┐  8. Create  ┌──────┐│
│  │ User │─────────────────►│ Backend  │────────────►│Stripe││
│  │      │  + PM ID + Cust  │   API    │  Subscription│     ││
│  │      │     ID           │          │             │      ││
│  │      │◄─────────────────│          │◄────────────│      ││
│  └──────┘  9. Success      └──────────┘  Sub Created└──────┘│
│     │                           │                             │
│     │ 10. Update Supabase       │                            │
│     ▼                           ▼                             │
│  ┌──────────┐              ┌──────────┐                     │
│  │ Navigate │              │ Webhook  │ 11. Update Status   │
│  │    to    │              │ Handler  │                     │
│  │ Discover │              └──────────┘                     │
│  └──────────┘                                                 │
└─────────────────────────────────────────────────────────────┘

Subscription Tiers

  1. Free Tier (Default)

    • Männer: 3 Telefonate & 3 Voice Messages pro Tag
    • Frauen: Unbegrenzte Telefonate & Voice Messages
    • Keine Super-Likes
    • Werbung wird angezeigt
    • Kein Invisible Mode
    • Kein Entfernungsfilter (< 50km)
  2. Premium Weekly (price_1Qk4K4L9v5z4q2x8Z1j5b3n7)

    • Preis: 0,99€ / Woche
    • Unbegrenzte Telefonate & Voice Messages
    • Invisible Mode
    • Priorisierte Matching-Algorithmen
    • Entfernungsfilter (< 50km)
    • Sehen wer dich geliked hat
    • Keine Werbung
    • 3 Super-Likes / Tag
  3. Premium Monthly (price_1Qk4K4L9v5z4q2x8Z1j5b3n8)

    • Preis: 2,99€ / Monat (25% günstiger als wöchentlich)
    • Alle Features des Weekly Plans
    • Maximale Ersparnis

Backend API Endpoints

POST /api/create-setup-intent

  • Zweck: Erstellt einen Stripe Customer und Setup Intent für Age Verification
  • Request Body: Leer
  • Response:
    {
      "clientSecret": "seti_xxx_secret_xxx",
      "customerId": "cus_xxx"
    }
  • Flow:
    1. Erstellt neuen Stripe Customer
    2. Erstellt Setup Intent für Karte ohne Zahlung
    3. Gibt Client Secret zurück

POST /api/create-subscription

  • Zweck: Erstellt eine Stripe Subscription
  • Request Body:
    {
      "customerId": "cus_xxx",
      "paymentMethodId": "pm_xxx",
      "priceId": "price_xxx",
      "email": "user@example.com",
      "userId": "uuid"
    }
  • Response:
    {
      "subscription": { /* Stripe Subscription Object */ },
      "customerId": "cus_xxx"
    }
  • Flow:
    1. Attached Payment Method an Customer
    2. Setzt Payment Method als Default
    3. Erstellt Subscription
    4. Returned Subscription Object

POST /api/cancel-subscription

  • Zweck: Kündigt eine Subscription (läuft bis Ende der Periode)
  • Request Body:
    {
      "userId": "uuid"
    }
  • Flow:
    1. Lädt User aus Supabase
    2. Findet aktive Subscription via stripe_customer_id
    3. Setzt cancel_at_period_end: true
    4. Updated Supabase subscription_status: 'canceling'

POST /api/reactivate-subscription

  • Zweck: Reaktiviert eine gekündigte Subscription
  • Request Body:
    {
      "userId": "uuid"
    }

Wichtige Implementierungsdetails

  1. Customer ID Management:

    • Customer ID wird bei Age Verification erstellt
    • Wird sofort in Supabase users.stripe_customer_id gespeichert
    • Wird vom Frontend an Subscription Endpoint übergeben
  2. Payment Method Storage:

    • Payment Method ID wird nach Age Verification im Frontend State gespeichert
    • Wird für Subscription verwendet (kein erneutes Kartenformular)
    • Wird an Stripe Customer attached
  3. State Flow:

    // OnboardingPage.tsx
    const [savedPaymentMethodId, setSavedPaymentMethodId] = useState<string | null>(null);
    const [savedCustomerId, setSavedCustomerId] = useState<string | null>(null);
    
    // Nach Age Verification
    setSavedPaymentMethodId(pmId);
    setSavedCustomerId(custId);
    await supabase.from('users').update({ stripe_customer_id: custId });
    
    // Beim Subscription Step
    <SubscriptionStep 
      initialPaymentMethodId={savedPaymentMethodId}
      initialCustomerId={savedCustomerId}
    />

Age Verification

Age Verification verwendet Stripe Setup Intents um sicherzustellen, dass User 18+ sind.

Warum Stripe für Age Verification?

  • Kreditkarten können nur von 18+ Personen besessen werden
  • Keine Zahlung erforderlich (Setup Intent)
  • Sichere PCI-DSS-konforme Verifizierung
  • Payment Method kann sofort für Subscription verwendet werden

Frontend Flow

AgeVerificationStep.tsx

// 1. Fetch Setup Intent
useEffect(() => {
  const response = await fetch('/api/create-setup-intent', {
    method: 'POST'
  });
  const { clientSecret, customerId } = await response.json();
  setClientSecret(clientSecret);
  setCustomerId(customerId);
}, []);

// 2. Confirm Card Setup (ohne Zahlung)
const handleSubmit = async (event) => {
  const result = await stripe.confirmCardSetup(clientSecret, {
    payment_method: {
      card: cardElement,
    },
  });
  
  if (result.error) {
    // Fehler (z.B. ungültige Karte)
    setError(result.error.message);
  } else {
    // Erfolg - Payment Method erstellt
    const pmId = result.setupIntent.payment_method;
    onVerified(pmId, customerId); // Weiter zu nächstem Step
  }
};

Wichtige Hinweise

  • Kein Betrag wird abgebucht - Es ist nur eine Verifizierung
  • Payment Method wird für spätere Subscription gespeichert
  • Customer ID wird in Supabase gespeichert für zukünftige Transaktionen

WebRTC Voice Calling

Das Calling System nutzt natives WebRTC (Peer-to-Peer) mit eigenem Signaling-Server (WebSocket) und TURN-Server (Coturn) für NAT-Traversal.

Architektur

┌─────────────────────────────────────────────────────────────┐
│                   WebRTC Call Flow                           │
├─────────────────────────────────────────────────────────────┤
│  ┌────────┐                                    ┌────────┐   │
│  │ User A │                                    │ User B │   │
│  └───┬────┘                                    └───┬────┘   │
│      │ 1. Offer (SDP)                              │         │
│      ├────────────────────► Signaling (WebSocket) ─┼────────►│
│      │                     │ 2. Forward Offer     │         │
│      │                     │ 3. Answer (SDP)       │         │
│      │◄────────────────────┼──────────────────────┤         │
│      │ 4. ICE Candidates   │   (optional: TURN)   │         │
│      │◄────────────────────┴─────────────────────►│         │
│      │ 5. Peer-to-Peer Audio (oder via TURN)      │         │
└─────────────────────────────────────────────────────────────┘

Komponenten

1. Signaling Server (backend/signaling/src/index.ts)

Zweck: Vermittelt Call-Signale zwischen Usern (Klingeln, Annehmen, Ablehnen)

// Socket.io Events
socket.on('call:signal', (data) => {
  // Signal an anderen User weiterleiten
  io.to(data.to).emit('call:incoming', {
    from: socket.userId,
    roomName: data.roomName,
    fromUser: data.fromUser
  });
});

socket.on('call:accept', (data) => {
  // Akzeptanz an Caller signalisieren
  io.to(data.callerId).emit('call:accepted', {
    roomName: data.roomName
  });
});

socket.on('call:reject', (data) => {
  // Ablehnung signalisieren
  io.to(data.callerId).emit('call:rejected');
});
2. CallService (src/features/calling/services/CallService.ts)

Zweck: Native WebRTC (RTCPeerConnection), Signaling über WebSocket, TURN (Coturn) für NAT-Traversal. Verwaltet Offer/Answer, ICE-Kandidaten und Audio-Streams.

3. Call Store (src/shared/stores/callStore.ts)

Zweck: Zentraler State für Call Management

interface CallStore {
  // State
  isInCall: boolean;
  incomingCall: IncomingCall | null;
  currentCall: CallInfo | null;
  
  // Actions
  initiateCall: (toUserId: string, toUser: CallUser) => Promise<void>;
  acceptCall: () => Promise<void>;
  rejectCall: () => void;
  endCall: () => void;
}

Call Flow - Schritt für Schritt

User A ruft User B an:

  1. User A klickt "Call"

    await callStore.initiateCall(userB.id, userBData);
  2. Signal wird über Signaling Server geschickt

    socket.emit('call:signal', {
      to: userB.id,
      roomName: 'room_xxx',
      fromUser: userAData
    });
  3. User B erhält Signal

    socket.on('call:incoming', (data) => {
      callStore.setIncomingCall({
        callerId: data.from,
        roomName: data.roomName,
        fromUser: data.fromUser
      });
      // Ringtone abspielen
      ringtoneService.playIncoming();
    });
  4. User B akzeptiert

    await callStore.acceptCall();
  5. Beide User bauen WebRTC-Verbindung auf

    • CallService sendet Answer, ICE-Kandidaten über Signaling
    • Bei Bedarf: TURN (Coturn) für NAT-Traversal
  6. Audio Stream läuft

    • Peer-to-Peer oder via TURN-Relay
  7. Call beenden

    await callStore.endCall();
    // - PeerConnection schließen, Signal an anderen User, Cleanup

TURN Server Configuration

Warum TURN Server?

  • NAT Traversal (wenn Peer-to-Peer nicht möglich)
  • Firewall Bypass
  • Guaranteed Connectivity

Eigener TURN Server (Coturn)
Vollständige Anleitung für Coolify + wizper.cloud: siehe backend/DEPLOY-TURN.md (Checkliste, EXTERNAL_IP, TURN_SECRET, use-auth-secret).

Konfigurationsdatei: backend/turn/turnserver.conf. Beispiel (ohne Secret – Secret kommt aus Env):

listening-port=3478
tls-listening-port=5349
realm=wizper.cloud
server-name=turn.wizper.cloud

# Authentication
lt-cred-mech
user=username:password

# Relay IPs
relay-ip=YOUR_SERVER_IP
external-ip=YOUR_PUBLIC_IP

Troubleshooting Voice Calls

Call klingelt nicht:

  • ✅ Prüfe ob Signaling Server läuft
  • ✅ Prüfe Socket.io Connection in Browser Console
  • ✅ Prüfe ob beide User online sind (Presence)

Kein Audio:

  • ✅ Prüfe Mikrofon-Berechtigungen (Browser/iOS/Android)
  • ✅ Prüfe Signaling-WebSocket und TURN (Admin: TURN Test)
  • ✅ Prüfe ob beide Participants im Room sind
  • ✅ Browser Console: room.localParticipant.isMicrophoneEnabled

Schlechte Audio-Qualität:

  • ✅ Netzwerk-Qualität prüfen
  • ✅ TURN Server verwendet? (Network Tab: STUN vs TURN)
  • ✅ TURN-Server erreichbar (siehe backend/DEPLOY-TURN.md)

Location Services

Das Location System ermöglicht es Usern, ihre Stadt anzuzeigen und Matches in der Nähe zu finden.

Architektur

┌─────────────────────────────────────────────────────────────┐
│                   Location Service Flow                      │
├─────────────────────────────────────────────────────────────┤
│                                                               │
│  User klickt "Enable Location"                              │
│           │                                                   │
│           ▼                                                   │
│  ┌────────────────────┐                                     │
│  │   1. High Accuracy │                                     │
│  │  (GPS + WiFi)      │                                     │
│  └────────┬───────────┘                                     │
│           │                                                   │
│           ├─── Success ────► Coordinates (lat, lon)         │
│           │                           │                      │
│           │                           ▼                      │
│           │                  ┌─────────────────┐            │
│           │                  │  2. Reverse     │            │
│           │                  │   Geocoding     │            │
│           │                  │  (Nominatim)    │            │
│           │                  └────────┬────────┘            │
│           │                           │                      │
│           │                           ▼                      │
│           │                      City Name                   │
│           │                                                   │
│           └─── Fail ────► ┌────────────────────┐           │
│                           │   3. Low Accuracy  │            │
│                           │   (WiFi/Cell)      │            │
│                           └─────────┬──────────┘            │
│                                     │                        │
│                                     ├─── Success ────►       │
│                                     │      Coordinates       │
│                                     │           │            │
│                                     │           ▼            │
│                                     │    Reverse Geocoding   │
│                                     │                        │
│                                     └─── Fail ────►          │
│                                            │                 │
│                                            ▼                 │
│                                  ┌─────────────────┐        │
│                                  │  4. IP Fallback │        │
│                                  │   (ipapi.co)    │        │
│                                  └────────┬────────┘        │
│                                           │                 │
│                                           ▼                 │
│                                      City + Coords          │
│                                                               │
└─────────────────────────────────────────────────────────────┘

Frontend Implementation

LocationStep.tsx

const handleEnableLocation = async () => {
  // 1. High Accuracy versuchen (GPS)
  try {
    const position = await new Promise<GeolocationPosition>((resolve, reject) => {
      navigator.geolocation.getCurrentPosition(resolve, reject, {
        enableHighAccuracy: true,
        timeout: 5000,
        maximumAge: 0
      });
    });
    
    const { latitude, longitude } = position.coords;
    const city = await reverseGeocode(latitude, longitude);
    
    setDetectedCity(city);
    setCoordinates({ lat: latitude, lon: longitude });
    return;
  } catch (highAccuracyError) {
    console.warn('High accuracy failed, trying low accuracy...');
  }
  
  // 2. Low Accuracy versuchen (WiFi/Cell)
  try {
    const position = await new Promise<GeolocationPosition>((resolve, reject) => {
      navigator.geolocation.getCurrentPosition(resolve, reject, {
        enableHighAccuracy: false,
        timeout: 15000,
        maximumAge: Infinity // Cache erlauben
      });
    });
    
    const { latitude, longitude } = position.coords;
    const city = await reverseGeocode(latitude, longitude);
    
    setDetectedCity(city);
    setCoordinates({ lat: latitude, lon: longitude });
    return;
  } catch (lowAccuracyError) {
    console.warn('Low accuracy failed, trying IP fallback...');
  }
  
  // 3. IP-based Fallback
  try {
    const response = await fetch('https://ipapi.co/json/');
    const data = await response.json();
    
    if (data.city && data.latitude && data.longitude) {
      setCoordinates({ lat: data.latitude, lon: data.longitude });
      setDetectedCity(data.city);
      return;
    }
  } catch (ipError) {
    throw new Error('All location methods failed');
  }
};

Reverse Geocoding

OpenStreetMap Nominatim API:

const reverseGeocode = async (
  latitude: number, 
  longitude: number
): Promise<string> => {
  const response = await fetch(
    `https://nominatim.openstreetmap.org/reverse?` +
    `lat=${latitude}&lon=${longitude}&format=json&addressdetails=1`,
    {
      headers: {
        'User-Agent': 'Wizper Dating App'
      }
    }
  );
  
  const data = await response.json();
  
  // Hierarchie für beste Stadt
  const city = data.address?.city ||        // Stadt
               data.address?.town ||        // Kleinstadt
               data.address?.village ||     // Dorf
               data.address?.municipality || // Gemeinde
               data.address?.county ||      // Landkreis
               'Unknown Location';
  
  return city;
};

Rate Limits:

  • Nominatim: 1 Request / Sekunde
  • User-Agent Header erforderlich
  • Kostenlos, kein API Key

Mobile Permissions

iOS (ios/App/App/Info.plist):

<key>NSLocationWhenInUseUsageDescription</key>
<string>Wizper uses your location to show your nearest city and help you find matches nearby.</string>

<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Wizper uses your location to show your nearest city and help you find matches nearby.</string>

Android (android/app/src/main/AndroidManifest.xml):

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

Troubleshooting Location

Browser zeigt Error Code 1 (Permission Denied):

  • Chrome: Klick auf 🔒 → Site Settings → Location → Allow
  • Safari: Safari → Settings → Websites → Location Services
  • macOS: System Settings → Privacy & Security → Location Services

Browser zeigt Error Code 2 (Position Unavailable):

  • System Location Services aus?
  • VPN/Proxy blockiert?
  • macOS: Chrome/Safari brauchen Location Permission
  • iOS Simulator: Nutze "Custom Location" in Simulator

Browser zeigt Error Code 3 (Timeout):

  • Langsame Internetverbindung
  • GPS Signal schwach (Indoor)
  • Timeout erhöhen: timeout: 30000

IP Fallback liefert falsche Stadt:

  • IP-based Location ist ungenau (kann 50km+ abweichen)
  • User sollte manuell korrigieren können
  • Alternative: Google Geocoding API (kostenpflichtig, genauer)

Voice Recording & Storage

User können 10-30 Sekunden Voice-Samples aufnehmen für ihr Profil.

Recording Flow

VoiceStep.tsx

const startRecording = async () => {
  // 1. Request Microphone Permission
  const stream = await navigator.mediaDevices.getUserMedia({ 
    audio: true 
  });
  
  // 2. Create MediaRecorder
  const mediaRecorder = new MediaRecorder(stream);
  const chunks: Blob[] = [];
  
  // 3. Collect audio data
  mediaRecorder.ondataavailable = (event) => {
    chunks.push(event.data);
  };
  
  // 4. On stop, create Blob
  mediaRecorder.onstop = () => {
    const blob = new Blob(chunks, { type: 'audio/webm' });
    const duration = Math.round((Date.now() - startTime) / 1000);
    
    setVoiceRecording({ blob, duration });
    stream.getTracks().forEach(track => track.stop());
  };
  
  // 5. Start recording
  mediaRecorder.start();
  startTime = Date.now();
};

Storage in Supabase

1. Upload zu Supabase Storage

const filename = `${userId}/${Date.now()}.webm`;

const { data: uploadData, error: uploadError } = await supabase.storage
  .from('voice-samples')
  .upload(filename, voiceBlob);

2. Metadata in Database

await supabase.from('voice_samples').insert({
  user_id: userId,
  storage_path: uploadData.path,
  duration_seconds: duration,
  is_primary: true
});

3. Abspielen

// Public URL holen
const { data } = supabase.storage
  .from('voice-samples')
  .getPublicUrl(storagePath);

// Audio Element erstellen
const audio = new Audio(data.publicUrl);
audio.play();

Supabase Storage Bucket Configuration

-- Bucket erstellen (via Supabase Dashboard)
INSERT INTO storage.buckets (id, name, public)
VALUES ('voice-samples', 'voice-samples', true);

-- RLS Policy für Upload
CREATE POLICY "Users can upload their own voice samples"
ON storage.objects FOR INSERT
TO authenticated
WITH CHECK (
  bucket_id = 'voice-samples' AND
  (storage.foldername(name))[1] = auth.uid()::text
);

-- RLS Policy für Read
CREATE POLICY "Voice samples are publicly readable"
ON storage.objects FOR SELECT
TO public
USING (bucket_id = 'voice-samples');

Mobile Permissions

iOS (Info.plist):

<key>NSMicrophoneUsageDescription</key>
<string>Wizper needs access to your microphone for voice calls and recording your profile intro.</string>

Android (AndroidManifest.xml):

<uses-permission android:name="android.permission.RECORD_AUDIO" />

Frauen-Schutz-System (Level 1000)

Ein mehrstufiges Sicherheitssystem, um Frauen volle Kontrolle über Interaktionen zu geben.

Asymmetrische Telefonie-Berechtigungen

┌─────────────────────────────────────────────────────────────┐
│               Anruf-Berechtigungs-System                     │
├─────────────────────────────────────────────────────────────┤
│                                                               │
│  Frauen → Männer:  ✅ Immer erlaubt                         │
│  Männer → Frauen:  🔒 Nur nach Like                         │
│  Gleiche Gender:   ✅ Immer erlaubt                         │
│                                                               │
│  ┌────────────────────────────────────────────────────────┐ │
│  │              Mann möchte Frau anrufen                   │ │
│  ├────────────────────────────────────────────────────────┤ │
│  │                                                          │ │
│  │  1. Mann sendet Sprachnachricht an Frau                │ │
│  │                    ↓                                    │ │
│  │  2. Frau hört Nachricht in ihrem Postfach              │ │
│  │                    ↓                                    │ │
│  │  3. Frau liked die Nachricht (❤️)                      │ │
│  │                    ↓                                    │ │
│  │  4. Anruf-Button wird für Mann aktiviert               │ │
│  │                                                          │ │
│  └────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘

Sprach-Postfach Features

Jede empfangene Sprachnachricht zeigt:

Information Beschreibung
Name Username des Absenders
Geschlecht Badge mit Farbe (♀ pink, ♂ blau)
Alter Berechnet aus Geburtstag
Stadt Wohnort des Absenders
Entfernung ~X km (wenn Location sichtbar)
Datum/Zeit Wann die Nachricht gesendet wurde

Aktions-Buttons pro Nachricht

Aktion Beschreibung
📞 Anrufen Startet Telefonat (bei Berechtigung)
❤️ Like Positives Feedback, aktiviert Anruf-Berechtigung für Absender
🚫 Blockieren User blockieren, keine weiteren Nachrichten
🚩 Melden Unangemessenes Verhalten melden

Datenbank-Struktur

-- Tracking von Likes auf Sprachnachrichten
voice_messages.is_liked    -- Boolean: Wurde Nachricht geliked?
voice_messages.liked_at    -- Timestamp: Wann wurde geliked?

-- User-Blockierungen
user_blocks (blocker_id, blocked_id, reason)

-- User-Meldungen
user_reports (reporter_id, reported_id, voice_message_id, reason, status)

-- Serverseitige Prüfung
can_initiate_call(caller_id, receiver_id) -- PostgreSQL Function

Sicherheitsmaßnahmen

  • Server-seitige Validierung: RLS Policies + PostgreSQL Function
  • Rate Limiting: Datenbank-Trigger verhindern Spam
  • Keine irreführenden CTAs: Button deaktiviert mit Erklärung
  • Moderation: Report-System für unangemessenes Verhalten

Progressive Web App (PWA)

Wizper ist als PWA implementiert und kann auf Android und iOS direkt installiert werden.

Features

  • Installierbar: App kann auf dem Homescreen hinzugefügt werden
  • Offline-fähig: Grundlegende Funktionen auch ohne Internet
  • Push-Benachrichtigungen: Eingehende Anrufe werden als Notifications angezeigt
  • Native App-Feeling: Vollbild-Modus ohne Browser-UI

Architektur

┌─────────────────────────────────────────────────────────────┐
│                     PWA Architecture                         │
├─────────────────────────────────────────────────────────────┤
│                                                               │
│  ┌────────────────┐                                          │
│  │  manifest.json │ ← App-Metadaten, Icons, Theme            │
│  └────────────────┘                                          │
│          │                                                    │
│          ▼                                                    │
│  ┌────────────────┐    ┌──────────────────┐                │
│  │ Service Worker │◄──►│  Cache Storage   │                │
│  │    (sw.js)     │    │ (Offline Assets) │                │
│  └────────────────┘    └──────────────────┘                │
│          │                                                    │
│          │ Push Events                                       │
│          ▼                                                    │
│  ┌────────────────┐                                          │
│  │ Notifications  │                                          │
│  │ (Incoming Calls)│                                         │
│  └────────────────┘                                          │
│                                                               │
│  ┌────────────────────────────────────────────────────────┐ │
│  │               PWAInstallPrompt Component                │ │
│  ├────────────────────────────────────────────────────────┤ │
│  │                                                          │ │
│  │  Android/Chrome:           iOS Safari:                  │ │
│  │  ┌──────────────┐         ┌──────────────────────┐    │ │
│  │  │beforeinstall │         │ Manual Instructions  │    │ │
│  │  │prompt Event  │         │ (Share → Add to Home)│    │ │
│  │  └──────────────┘         └──────────────────────┘    │ │
│  │                                                          │ │
│  └────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘

Installation auf verschiedenen Plattformen

Android (Chrome):

  1. Login-Seite besuchen
  2. "App installieren" Button klicken
  3. Chrome zeigt nativen Install-Dialog
  4. "Installieren" bestätigen
  5. App erscheint auf dem Homescreen

iOS (Safari):

  1. Login-Seite besuchen
  2. "So geht's" Button klicken
  3. Anleitung befolgen:
    • Share-Button (⬆) tippen
    • "Zum Home-Bildschirm" wählen
    • "Hinzufügen" bestätigen

Desktop (Chrome/Edge):

  1. Login-Seite besuchen
  2. "App installieren" Button klicken
  3. Im Dialog "Installieren" bestätigen

Wichtige Dateien

Datei Zweck
public/manifest.webmanifest PWA-Konfiguration (Name, Icons, Theme)
public/sw.js Service Worker (Caching, Push Notifications)
public/icons/ App-Icons in verschiedenen Größen
src/shared/components/PWAInstallPrompt.tsx Install-Banner Komponente

Service Worker Funktionen

// Caching-Strategie: Network First, Cache Fallback
// - Versucht zuerst vom Netzwerk zu laden
// - Bei Offline: Cached Version verwenden
// - API-Requests werden nicht gecached

// Push Notifications für:
// - Eingehende Anrufe
// - Mit Accept/Decline Actions

Icons generieren (Produktion)

Für die Produktion sollten PNG-Icons generiert werden:

# Mit einem Tool wie sharp oder ImageMagick:
# SVG zu PNG konvertieren in verschiedenen Größen
convert public/icons/icon.svg -resize 192x192 public/icons/icon-192x192.png
convert public/icons/icon.svg -resize 512x512 public/icons/icon-512x512.png

Oder Online-Tools verwenden:

Troubleshooting PWA

Install-Button erscheint nicht (Android):

  • ✅ HTTPS erforderlich (localhost ist Ausnahme)
  • ✅ Manifest korrekt verlinkt?
  • ✅ Service Worker registriert?
  • ✅ Chrome DevTools → Application → Manifest prüfen

iOS zeigt keine Install-Option:

  • iOS unterstützt keine automatische Installation
  • User muss manuell über Share → Add to Home Screen gehen
  • Die App zeigt entsprechende Anleitung

Offline funktioniert nicht:

  • ✅ Service Worker aktiv? (DevTools → Application → Service Workers)
  • ✅ Cache gefüllt? (DevTools → Application → Cache Storage)
  • ✅ Network-Tab: Requests von Service Worker?

Push Notifications kommen nicht an:

  • ✅ Notification Permission erteilt?
  • ✅ Service Worker aktiv?
  • ✅ Push-Subscription erfolgreich?

🚀 Lokales Setup

Voraussetzungen

1. Repository klonen

git clone https://github.com/yourusername/wizper.git
cd wizper

2. Dependencies installieren

# Frontend
npm install

# Backend API
cd api
npm install
cd ..

# Signaling Server
cd backend/signaling
npm install
cd ../..

3. Umgebungsvariablen einrichten

Frontend (.env):

VITE_SUPABASE_URL=https://your-project.supabase.co
VITE_SUPABASE_ANON_KEY=your-anon-key
VITE_STRIPE_PUBLISHABLE_KEY=pk_test_xxx
VITE_STRIPE_WEEKLY_PRICE_ID=price_xxx
VITE_STRIPE_MONTHLY_PRICE_ID=price_xxx

Backend API (api/.env):

PORT=3001
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
STRIPE_SECRET_KEY=sk_test_xxx
STRIPE_WEBHOOK_SECRET=whsec_xxx
VITE_APP_URL=http://localhost:5173

Signaling Server (backend/signaling/.env):

PORT=3002

4. Supabase Setup

  1. Erstelle neues Supabase Projekt
  2. Führe Migrations aus:
    cd supabase
    # Kopiere SQL aus migrations/ ins Supabase SQL Editor
  3. Erstelle Storage Buckets:
    • voice-samples (public)
    • profile-pictures (public)

5. Stripe Setup

  1. Erstelle Produkte & Preise:
    • Premium Weekly (0,99€ / Woche)
    • Premium Monthly (2,99€ / Monat)
  2. Kopiere Price IDs in .env
  3. Setup Webhook:
    • URL: https://your-api.com/api/webhook
    • Events: customer.subscription.created, customer.subscription.updated, customer.subscription.deleted

6. App starten

# Terminal 1: Frontend
npm run dev
# → http://localhost:5173

# Terminal 2: Backend API
cd api
npm start
# → http://localhost:3001

# Terminal 3: Signaling Server
cd backend/signaling
npm start
# → http://localhost:3002

7. Mobile Setup (Optional)

iOS:

npm run build:ios
npm run cap:ios
# Xcode öffnet sich

Android:

npm run build:android
npm run cap:android
# Android Studio öffnet sich

🌍 Umgebungsvariablen

Frontend (.env)

Variable Beschreibung Beispiel
VITE_SUPABASE_URL Supabase Project URL https://xxx.supabase.co
VITE_SUPABASE_ANON_KEY Supabase Anon/Public Key eyJh...
VITE_STRIPE_PUBLISHABLE_KEY Stripe Public Key pk_test_... oder pk_live_...
VITE_STRIPE_WEEKLY_PRICE_ID Stripe Price ID (Weekly) price_1Qk4K4...
VITE_STRIPE_MONTHLY_PRICE_ID Stripe Price ID (Monthly) price_1Qk4K4...
VITE_API_URL Backend API URL (Prod only) https://api.wizper.cloud
VITE_SIGNALING_URL WebSocket Signaling URL wss://signaling.wizper.cloud
VITE_TURN_URL TURN Server URL turn:turn.wizper.cloud:3478
VITE_TURN_SECRET TURN Secret (zeitbasierte Auth) siehe backend/DEPLOY-TURN.md

Backend API (api/.env)

Variable Beschreibung Beispiel
PORT API Server Port 3001
SUPABASE_URL Supabase Project URL https://xxx.supabase.co
SUPABASE_SERVICE_ROLE_KEY Supabase Service Role Key (Admin) eyJh...
STRIPE_SECRET_KEY Stripe Secret Key sk_test_... oder sk_live_...
STRIPE_WEBHOOK_SECRET Stripe Webhook Secret whsec_...
VITE_APP_URL Frontend URL (for redirects) https://wizper.cloud

Signaling Server (backend/signaling/.env)

Variable Beschreibung Beispiel
PORT Signaling Server Port 3002
CORS_ORIGIN Allowed CORS Origins https://wizper.cloud,http://localhost:5173

📦 Deployment

Frontend (Statische Seite)

Empfohlene Plattformen:

  • Vercel
  • Netlify
  • Cloudflare Pages

Build:

npm run build
# Output: dist/

Wichtig:

  • Setze VITE_API_URL auf Production API URL
  • Configure SPA Fallback (alle Routes → index.html)

Backend API (Node.js Server)

Empfohlene Plattformen:

  • Railway
  • Fly.io
  • Render
  • DigitalOcean App Platform

Dockerfile:

FROM node:22-alpine AS builder
WORKDIR /app
COPY api/package*.json ./
RUN npm ci
COPY api/ .
CMD ["npm", "start"]

Wichtig:

  • Stripe Webhook URL konfigurieren
  • Environment Variables setzen
  • Port aus process.env.PORT lesen

Signaling Server

Gleiche Plattform wie API Server

Dockerfile:

FROM node:22-alpine
WORKDIR /app
COPY backend/signaling/package*.json ./
RUN npm ci
COPY backend/signaling/ .
CMD ["npm", "start"]

Mobile Apps

iOS:

npm run build:ios
# In Xcode:
# 1. Archive erstellen
# 2. Distribute → App Store Connect
# 3. Submit for Review

Android:

npm run build:android
# In Android Studio:
# 1. Build → Generate Signed Bundle
# 2. Upload to Google Play Console
# 3. Submit for Review

🐛 Troubleshooting

Payment Issues

"Customer ID and Payment Method ID are required"

  • ✅ Check: Age Verification completed?
  • ✅ Check: stripe_customer_id in Supabase users table?
  • ✅ Check: initialCustomerId passed to SubscriptionStep?
  • ✅ Check: Backend logs for Stripe API errors

Age Verification fails

  • ✅ Check: Stripe keys correct? (Test mode vs Live mode)
  • ✅ Check: Card valid? (Use Stripe test cards in dev)
  • ✅ Check: Browser console for Stripe.js errors

Voice Call Issues

Call doesn't ring

  • ✅ Check: Signaling Server running?
  • ✅ Check: Socket.io connected? (Browser console)
  • ✅ Check: Both users online? (Presence)
  • ✅ Check: Firewall blocking WebSocket?

No audio

  • ✅ Check: Microphone permissions granted?
  • ✅ Check: Signaling WebSocket connected? TURN erreichbar? (Admin: TURN Test)
  • ✅ Check: PeerConnection state in console

Poor audio quality

  • ✅ Check: Network quality (3G vs WiFi)
  • ✅ Check: TURN server used? (Chrome DevTools → Network)
  • ✅ Check: TURN-Server erreichbar (backend/DEPLOY-TURN.md)

Location Issues

"Position unavailable"

  • ✅ Check: System Location Services enabled?
  • ✅ Check: Browser has location permission?
  • ✅ macOS: System Settings → Privacy → Location Services
  • ✅ Browser: Site Settings → Location → Allow

Wrong city detected

  • ✅ IP-based location can be 50km+ off
  • ✅ GPS/WiFi location more accurate
  • ✅ User can manually correct in profile

Database Issues

"Invalid input value for enum"

  • ✅ Check: Run latest migrations?
  • ✅ Check: ENUM updated in Supabase?
  • ✅ Check: TypeScript types match DB schema?

RLS Policy errors

  • ✅ Check: User authenticated?
  • ✅ Check: Policy allows operation?
  • ✅ Supabase Dashboard → Database → Policies

Build/Deploy Issues

TypeScript errors

  • ✅ Run: npm run build locally first
  • ✅ Check: All dependencies installed?
  • ✅ Check: TypeScript version matches?

Environment variables missing

  • ✅ Check: All required vars set?
  • ✅ Check: VITE_ prefix for frontend vars?
  • ✅ Check: Vercel/Railway dashboard for correct values

📚 Weitere Ressourcen


📝 Changelog

Januar 2026

  • ✅ Multi-Step Onboarding implementiert
  • ✅ Age Verification mit Stripe Setup Intent
  • ✅ Premium Subscriptions (Weekly/Monthly)
  • ✅ Native WebRTC Voice Calling (Signaling + TURN/Coturn)
  • ✅ Location Services mit 3-Tier Fallback
  • ✅ Voice Recording & Playback
  • ✅ Customer ID Management Fix
  • ✅ Discovery Page Reload Bug Fix
  • ✅ Voice Message Inbox (Postfach für Sprachnachrichten)
  • ✅ Comprehensive README Documentation
  • ✅ Fix: voice_messages query 400 Bad Request - Spaltenname is_read zu is_listened korrigiert (Datenbank verwendet is_listened)
  • ✅ PWA Implementation - Progressive Web App mit Install-Prompts für Android und iOS
  • ✅ Erweitertes Sprach-Postfach mit Absender-Kontext (Alter, Stadt, Distanz, Geschlecht)
  • ✅ Geschütztes Telefonie-System: Frauen kontrollieren, wer sie anrufen darf
  • ✅ Like/Block/Report Aktionen für jede Sprachnachricht
  • ✅ Fix: Logout hängt bei "Loading..." - Timeout für supabase.auth.signOut() hinzugefügt, localStorage-Tokens werden sofort gelöscht

Erstellt mit ❤️ für authentische Verbindungen

Bei Fragen oder Issues: GitHub Issues

About

Your Next Voice Match is Waiting.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages