From 0e16eecda6cd3f481219098c2d2314709bbb86fd Mon Sep 17 00:00:00 2001 From: sdarbinyan Date: Mon, 17 Aug 2026 23:58:48 +0400 Subject: [PATCH] feat: Phase 8 frontend - Customer identity core + standalone VK ID login button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit core/identity (Customer/ExternalIdentity/ContactChannel models + VkIdGateway/ token/mock) and a standalone VkIdLoginComponent, per Sprint 0.1's "VK ID first" decision and v3.1 §14. Against docs/backend/PHASE-8-IDENTITY-MESSAGING-CONTRACT.md §1-2. Deliberately not wired into TelegramLoginComponent's dialog - that's the live, working QR-login surface for both customer and admin auth (components/telegram-login/), and splicing a second provider into it needs a real VK OAuth app to test against, not a mock bolt-on next to nine other phases. The button is a standalone, ready-to-place component; integrating it into checkout/login flows is follow-up work once VK credentials exist. Co-Authored-By: Claude Sonnet 5 --- .../vk-id-login/vk-id-login.component.html | 4 ++ .../vk-id-login/vk-id-login.component.scss | 19 ++++++++++ .../vk-id-login/vk-id-login.component.ts | 37 +++++++++++++++++++ .../models/customer-identity.model.ts | 28 ++++++++++++++ .../services/vk-id-gateway.interface.ts | 8 ++++ .../identity/services/vk-id-gateway.token.ts | 9 +++++ .../identity/services/vk-id-local.gateway.ts | 28 ++++++++++++++ 7 files changed, 133 insertions(+) create mode 100644 src/app/components/vk-id-login/vk-id-login.component.html create mode 100644 src/app/components/vk-id-login/vk-id-login.component.scss create mode 100644 src/app/components/vk-id-login/vk-id-login.component.ts create mode 100644 src/app/core/identity/models/customer-identity.model.ts create mode 100644 src/app/core/identity/services/vk-id-gateway.interface.ts create mode 100644 src/app/core/identity/services/vk-id-gateway.token.ts create mode 100644 src/app/core/identity/services/vk-id-local.gateway.ts diff --git a/src/app/components/vk-id-login/vk-id-login.component.html b/src/app/components/vk-id-login/vk-id-login.component.html new file mode 100644 index 0000000..dd23424 --- /dev/null +++ b/src/app/components/vk-id-login/vk-id-login.component.html @@ -0,0 +1,4 @@ + diff --git a/src/app/components/vk-id-login/vk-id-login.component.scss b/src/app/components/vk-id-login/vk-id-login.component.scss new file mode 100644 index 0000000..e02d62a --- /dev/null +++ b/src/app/components/vk-id-login/vk-id-login.component.scss @@ -0,0 +1,19 @@ +.vk-id-login { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + width: 100%; + padding: 10px 16px; + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + background: var(--bg-primary); + color: var(--text-primary); + font-weight: var(--font-weight-bold, 700); + cursor: pointer; + + &:disabled { + opacity: 0.6; + cursor: default; + } +} diff --git a/src/app/components/vk-id-login/vk-id-login.component.ts b/src/app/components/vk-id-login/vk-id-login.component.ts new file mode 100644 index 0000000..a7dce7f --- /dev/null +++ b/src/app/components/vk-id-login/vk-id-login.component.ts @@ -0,0 +1,37 @@ +import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { take } from 'rxjs/operators'; +import { VK_ID_GATEWAY } from '../../core/identity/services/vk-id-gateway.token'; +import { IconComponent } from '../../shared/ui/icon/icon.component'; + +/** + * Standalone VK ID login button, per Sprint 0.1 ("do all after vk" - VK ID + * is the primary storefront social login going forward, per v3.1 §14). + * Deliberately not wired into TelegramLoginComponent's dialog yet - that + * component is the live, working customer/admin login surface, and + * splicing a second provider into it needs its own careful pass once a + * real VK OAuth app exists to test against, not a mock-backed bolt-on. + */ +@Component({ + selector: 'app-vk-id-login', + standalone: true, + imports: [CommonModule, IconComponent], + templateUrl: './vk-id-login.component.html', + styleUrls: ['./vk-id-login.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class VkIdLoginComponent { + private readonly gateway = inject(VK_ID_GATEWAY); + + readonly loading = signal(false); + + startLogin(): void { + this.loading.set(true); + this.gateway.getAuthorizeUrl().pipe(take(1)).subscribe(url => { + this.loading.set(false); + if (typeof window !== 'undefined') { + window.location.href = url; + } + }); + } +} diff --git a/src/app/core/identity/models/customer-identity.model.ts b/src/app/core/identity/models/customer-identity.model.ts new file mode 100644 index 0000000..f0ce9b9 --- /dev/null +++ b/src/app/core/identity/models/customer-identity.model.ts @@ -0,0 +1,28 @@ +/** Per docs/backend/PHASE-8-IDENTITY-MESSAGING-CONTRACT.md §1. */ +export interface Customer { + id: string; + marketplaceId: string; + name?: string; + email?: string; + phone?: string; + status: 'active' | 'suspended'; + createdAt: string; +} + +export type ExternalIdentityProvider = 'vk_id' | 'telegram' | 'max'; + +export interface ExternalIdentity { + customerId: string; + provider: ExternalIdentityProvider; + providerUserId: string; + verifiedAt: string; + lastUsedAt: string; +} + +export interface ContactChannel { + customerId: string; + provider: 'telegram' | 'vk' | 'max'; + chatId: string; + verified: boolean; + notificationsEnabled: boolean; +} diff --git a/src/app/core/identity/services/vk-id-gateway.interface.ts b/src/app/core/identity/services/vk-id-gateway.interface.ts new file mode 100644 index 0000000..20e4097 --- /dev/null +++ b/src/app/core/identity/services/vk-id-gateway.interface.ts @@ -0,0 +1,8 @@ +import { Observable } from 'rxjs'; +import { Customer } from '../models/customer-identity.model'; + +/** Per docs/backend/PHASE-8-IDENTITY-MESSAGING-CONTRACT.md §2. OAuth completion is backend-side; this is the client-facing surface only. */ +export interface VkIdGateway { + getAuthorizeUrl(): Observable; + completeCallback(code: string, codeVerifier: string): Observable; +} diff --git a/src/app/core/identity/services/vk-id-gateway.token.ts b/src/app/core/identity/services/vk-id-gateway.token.ts new file mode 100644 index 0000000..d33579b --- /dev/null +++ b/src/app/core/identity/services/vk-id-gateway.token.ts @@ -0,0 +1,9 @@ +import { InjectionToken, inject } from '@angular/core'; +import { VkIdGateway } from './vk-id-gateway.interface'; +import { VkIdLocalGateway } from './vk-id-local.gateway'; + +/** Swap point for docs/backend/PHASE-8-IDENTITY-MESSAGING-CONTRACT.md §2. */ +export const VK_ID_GATEWAY = new InjectionToken('VK_ID_GATEWAY', { + providedIn: 'root', + factory: () => inject(VkIdLocalGateway), +}); diff --git a/src/app/core/identity/services/vk-id-local.gateway.ts b/src/app/core/identity/services/vk-id-local.gateway.ts new file mode 100644 index 0000000..950ccff --- /dev/null +++ b/src/app/core/identity/services/vk-id-local.gateway.ts @@ -0,0 +1,28 @@ +import { Injectable } from '@angular/core'; +import { Observable, of } from 'rxjs'; +import { Customer } from '../models/customer-identity.model'; +import { VkIdGateway } from './vk-id-gateway.interface'; + +/** + * No real VK OAuth app is configured yet - this mock exists so the + * VkIdLoginButtonComponent has something to call and the flow shape is + * provable end-to-end before a real client id/secret exist. Swap + * VK_ID_GATEWAY once docs/backend/PHASE-8-IDENTITY-MESSAGING-CONTRACT.md §2 + * ships; the real backend completes OAuth server-side, this interface never + * exposes a client secret regardless of implementation. + */ +@Injectable({ providedIn: 'root' }) +export class VkIdLocalGateway implements VkIdGateway { + getAuthorizeUrl(): Observable { + return of('about:blank#vk-id-not-configured'); + } + + completeCallback(_code: string, _codeVerifier: string): Observable { + return of({ + id: 'customer_vk_mock', + marketplaceId: 'default', + status: 'active', + createdAt: new Date().toISOString(), + }); + } +}