Skip to content

QR Code Share Token — Resolve & Add Friend Flow

Scope: Handles the processing when a user scans a QR code, imports a photo from the gallery, or taps on another user's Universal Link. It exclusively uses the resolve API to fetch the Target ID. Legacy token formats are no longer supported (No Legacy Path).

flowchart TD
    classDef input fill:#EDE9FE,stroke:#7C3AED,color:#5B21B6,stroke-width:2px;
    classDef client fill:#E0F2FE,stroke:#0284C7,color:#0369A1,stroke-width:2px;
    classDef server fill:#D1FAE5,stroke:#059669,color:#047857,stroke-width:2px;
    classDef error fill:#FEE2E2,stroke:#DC2626,color:#B91C1C,stroke-width:2px;

    %% ────────────────────────────────────────────────────────
    %% 1. INPUT SOURCES
    %% ────────────────────────────────────────────────────────
    subgraph Inputs ["Input Sources"]
        direction LR
        Cam(["📷 Camera Scan"]):::input
        Photo(["🖼️ Photo Import"]):::input
        Link(["🔗 Universal Link / ecall://"]):::input
    end

    %% ────────────────────────────────────────────────────────
    %% 2. PARSE TOKEN
    %% ────────────────────────────────────────────────────────
    subgraph App ["iOS Client - AddFriendViewModel"]
        Cam --> Extract["Extract {token}<br/>from URL string"]:::client
        Photo --> Extract
        Link --> Extract

        Extract --> TriggerAPI("POST /app/api/share-token/resolve<br/>body: { token }"):::client
    end

    %% ────────────────────────────────────────────────────────
    %% 3. BACKEND RESOLVE
    %% ────────────────────────────────────────────────────────
    subgraph Backend ["Backend / Token Service"]
        TriggerAPI --> Validate{"Validate Token"}:::server
        Validate -->|Check 1| TTL["Within 24h?"]:::server
        Validate -->|Check 2| Used["Not yet used?"]:::server

        TTL --> ChecksDone
        Used --> ChecksDone

        ChecksDone(("Verify")):::server -->|Failed| Err410["HTTP 410 Gone<br/>Token expired or used"]:::server
        ChecksDone -->|Pass| Success200["Return: { token, displayName }<br/>(Token is NOT burned here)"]:::server
    end

    %% ────────────────────────────────────────────────────────
    %% 4. RESOLVE RESULTS
    %% ────────────────────────────────────────────────────────
    Err410 --> UIErr(["UI: Show 'Link Expired / Used' alert"]):::error
    Success200 --> UIContact["Update UI: Show Profile<br/>Store token for friend request"]:::client

    %% ────────────────────────────────────────────────────────
    %% 5. FRIEND REQUEST ACTION
    %% ────────────────────────────────────────────────────────
    subgraph FriendAction ["Friend Request Phase"]
        UIContact --> UserDecision{"User Action"}:::client

        UserDecision -->|"Cancel"| UICancel(["Discard token<br/>Dismiss Sheet"]):::client
        UserDecision -->|"Add Friend"| FriendReq(["POST /friend-request/by-token<br/>body: { token }"]):::client

        FriendReq --> VerifySession{"Validate Token<br/>(One-time use)"}:::server
        VerifySession -->|"Expired (24h)<br/>or Used"| ErrSession["HTTP 410 / 401<br/>Token Expired"]:::server
        VerifySession -->|"Valid"| ExecuteAdd["Map token to targetUserId<br/>Create friendship<br/>Burn token (used_count = 1)"]:::server

        ErrSession --> UISessionErr(["UI: 'Session expired. Please scan again.'"]):::error
        ExecuteAdd --> UIDone(["UI: 'Friend Request Sent'"]):::client
    end