feat: account linking and merge UI for cabinet

Add Connected Accounts page (link/unlink OAuth providers), Link OAuth
Callback handler, and Merge Accounts page with subscription comparison
and user choice. Includes API methods, TypeScript types, routes in
App.tsx, navigation from Profile, and i18n for all 4 locales (ru, en,
zh, fa). Merge page works without JWT auth (validated by merge token).
This commit is contained in:
Fringg
2026-03-04 07:25:45 +03:00
parent 8157ca5f02
commit 93f97d45be
12 changed files with 1253 additions and 2 deletions

View File

@@ -1,5 +1,15 @@
import apiClient from './client';
import type { AuthResponse, OAuthProvider, RegisterResponse, TokenResponse, User } from '../types';
import type {
AuthResponse,
LinkCallbackResponse,
LinkedProvidersResponse,
MergePreviewResponse,
MergeResponse,
OAuthProvider,
RegisterResponse,
TokenResponse,
User,
} from '../types';
export const authApi = {
// Telegram WebApp authentication
@@ -190,4 +200,64 @@ export const authApi = {
);
return response.data;
},
// Account linking
getLinkedProviders: async (): Promise<LinkedProvidersResponse> => {
const response = await apiClient.get<LinkedProvidersResponse>(
'/cabinet/auth/account/linked-providers',
);
return response.data;
},
linkProviderInit: async (provider: string): Promise<{ authorize_url: string; state: string }> => {
const response = await apiClient.get<{ authorize_url: string; state: string }>(
`/cabinet/auth/account/link/${encodeURIComponent(provider)}/init`,
);
return response.data;
},
linkProviderCallback: async (
provider: string,
code: string,
state: string,
deviceId?: string,
): Promise<LinkCallbackResponse> => {
const response = await apiClient.post<LinkCallbackResponse>(
`/cabinet/auth/account/link/${encodeURIComponent(provider)}/callback`,
{
code,
state,
device_id: deviceId,
},
);
return response.data;
},
unlinkProvider: async (provider: string): Promise<{ success: boolean }> => {
const response = await apiClient.post<{ success: boolean }>(
`/cabinet/auth/account/unlink/${encodeURIComponent(provider)}`,
);
return response.data;
},
// Account merge (no JWT required)
getMergePreview: async (mergeToken: string): Promise<MergePreviewResponse> => {
const response = await apiClient.get<MergePreviewResponse>(
`/cabinet/auth/merge/${encodeURIComponent(mergeToken)}`,
);
return response.data;
},
executeMerge: async (
mergeToken: string,
keepSubscriptionFrom: number,
): Promise<MergeResponse> => {
const response = await apiClient.post<MergeResponse>(
`/cabinet/auth/merge/${encodeURIComponent(mergeToken)}`,
{
keep_subscription_from: keepSubscriptionFrom,
},
);
return response.data;
},
};

View File

@@ -75,6 +75,7 @@ const AUTH_ENDPOINTS = [
'/cabinet/auth/password/forgot',
'/cabinet/auth/password/reset',
'/cabinet/auth/oauth/',
'/cabinet/auth/merge/',
];
function isAuthEndpoint(url: string | undefined): boolean {