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(),
+ });
+ }
+}