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

@@ -3598,6 +3598,30 @@
"promoOffers": "Promo Offers",
"promoOffersDesc": "Receive special offers and discounts",
"unavailable": "Notification settings unavailable"
},
"accounts": {
"title": "Connected Accounts",
"subtitle": "Manage your sign-in methods",
"linked": "Connected",
"notLinked": "Not connected",
"link": "Connect",
"unlink": "Disconnect",
"unlinkConfirm": "Are you sure? You won't be able to sign in with this service after disconnecting.",
"cannotUnlinkLast": "Cannot disconnect the last sign-in method",
"linkSuccess": "Account connected successfully",
"unlinkSuccess": "Account disconnected successfully",
"linkError": "Failed to connect account",
"unlinkError": "Failed to disconnect account",
"goToAccounts": "Connected Accounts",
"linking": "Linking account...",
"providers": {
"telegram": "Telegram",
"email": "Email",
"google": "Google",
"yandex": "Yandex",
"discord": "Discord",
"vk": "VK"
}
}
},
"theme": {
@@ -3807,5 +3831,32 @@
"reason": "Reason",
"contactSupport": "If you believe this is an error, please contact support."
}
},
"merge": {
"title": "Merge Accounts",
"description": "This sign-in method is already linked to another account. You can merge both accounts into one.",
"currentAccount": "Your current account",
"foundAccount": "Found account",
"authMethods": "Sign-in methods",
"subscription": "Subscription",
"noSubscription": "No subscription",
"traffic": "Traffic",
"devices": "Devices",
"balance": "Balance",
"until": "until {{date}}",
"keepThisSubscription": "Keep this subscription",
"chooseSubscription": "Choose which subscription to keep",
"afterMerge": "After merging",
"allAuthMethodsMerged": "All sign-in methods will be combined",
"balanceSummed": "Balance: {{amount}} ₽ (combined)",
"unselectedSubscriptionDeleted": "The unselected subscription will be deleted",
"historyPreserved": "Transaction history will be preserved",
"confirm": "Merge Accounts",
"cancel": "Cancel",
"expired": "The merge link has expired. Please try again.",
"success": "Accounts merged successfully!",
"error": "Failed to merge accounts. Please try again later.",
"expiresIn": "Expires in {{minutes}}",
"merging": "Merging..."
}
}

View File

@@ -3034,6 +3034,29 @@
"promoOffers": "پیشنهادات تبلیغاتی",
"promoOffersDesc": "دریافت پیشنهادات ویژه و تخفیف‌ها",
"unavailable": "تنظیمات اعلان در دسترس نیست"
},
"accounts": {
"title": "حساب‌های متصل",
"subtitle": "مدیریت روش‌های ورود",
"linked": "متصل",
"notLinked": "متصل نیست",
"link": "اتصال",
"unlink": "قطع اتصال",
"unlinkConfirm": "آیا مطمئن هستید؟ پس از قطع اتصال نمی‌توانید از طریق این سرویس وارد شوید.",
"cannotUnlinkLast": "نمی‌توان آخرین روش ورود را قطع کرد",
"linkSuccess": "حساب با موفقیت متصل شد",
"unlinkSuccess": "اتصال حساب با موفقیت قطع شد",
"linkError": "اتصال حساب ناموفق بود",
"unlinkError": "قطع اتصال ناموفق بود",
"goToAccounts": "حساب‌های متصل",
"providers": {
"telegram": "تلگرام",
"email": "ایمیل",
"google": "گوگل",
"yandex": "یاندکس",
"discord": "دیسکورد",
"vk": "VK"
}
}
},
"theme": {
@@ -3358,5 +3381,32 @@
"reason": "دلیل",
"contactSupport": "اگر فکر می‌کنید این اشتباه است، با پشتیبانی تماس بگیرید."
}
},
"merge": {
"title": "ادغام حساب‌ها",
"description": "این روش ورود به حساب دیگری متصل است. می‌توانید هر دو حساب را ادغام کنید.",
"currentAccount": "حساب فعلی شما",
"foundAccount": "حساب یافت شده",
"authMethods": "روش‌های ورود",
"subscription": "اشتراک",
"noSubscription": "بدون اشتراک",
"traffic": "ترافیک",
"devices": "دستگاه‌ها",
"balance": "موجودی",
"until": "تا {{date}}",
"keepThisSubscription": "نگه داشتن این اشتراک",
"chooseSubscription": "اشتراک مورد نظر را انتخاب کنید",
"afterMerge": "پس از ادغام",
"allAuthMethodsMerged": "تمام روش‌های ورود ادغام خواهند شد",
"balanceSummed": "موجودی: {{amount}} ₽ (مجموع)",
"unselectedSubscriptionDeleted": "اشتراک انتخاب نشده حذف خواهد شد",
"historyPreserved": "تاریخچه تراکنش‌ها حفظ خواهد شد",
"confirm": "ادغام حساب‌ها",
"cancel": "لغو",
"expired": "لینک ادغام منقضی شده است. لطفاً دوباره تلاش کنید.",
"success": "حساب‌ها با موفقیت ادغام شدند!",
"error": "ادغام ناموفق بود. لطفاً بعداً دوباره تلاش کنید.",
"expiresIn": "{{minutes}} دقیقه باقی مانده",
"merging": "در حال ادغام..."
}
}

View File

@@ -4158,6 +4158,30 @@
"promoOffers": "Промо-предложения",
"promoOffersDesc": "Получать специальные предложения и скидки",
"unavailable": "Настройки уведомлений недоступны"
},
"accounts": {
"title": "Привязанные аккаунты",
"subtitle": "Управляйте способами входа в кабинет",
"linked": "Привязан",
"notLinked": "Не привязан",
"link": "Привязать",
"unlink": "Отвязать",
"unlinkConfirm": "Вы уверены? После отвязки вы не сможете входить через этот сервис.",
"cannotUnlinkLast": "Нельзя отвязать последний способ входа",
"linkSuccess": "Аккаунт успешно привязан",
"unlinkSuccess": "Аккаунт успешно отвязан",
"linkError": "Не удалось привязать аккаунт",
"unlinkError": "Не удалось отвязать аккаунт",
"goToAccounts": "Привязанные аккаунты",
"linking": "Привязка аккаунта...",
"providers": {
"telegram": "Telegram",
"email": "Email",
"google": "Google",
"yandex": "Яндекс",
"discord": "Discord",
"vk": "VK"
}
}
},
"theme": {
@@ -4370,5 +4394,32 @@
"reason": "Причина",
"contactSupport": "Если вы считаете, что это ошибка, обратитесь в поддержку."
}
},
"merge": {
"title": "Объединение аккаунтов",
"description": "Этот способ входа уже привязан к другому аккаунту. Вы можете объединить два аккаунта в один.",
"currentAccount": "Ваш текущий аккаунт",
"foundAccount": "Найденный аккаунт",
"authMethods": "Способы входа",
"subscription": "Подписка",
"noSubscription": "Нет подписки",
"traffic": "Трафик",
"devices": "Устройства",
"balance": "Баланс",
"until": "до {{date}}",
"keepThisSubscription": "Оставить эту подписку",
"chooseSubscription": "Выберите подписку, которую хотите сохранить",
"afterMerge": "После объединения",
"allAuthMethodsMerged": "Все способы входа объединятся",
"balanceSummed": "Баланс: {{amount}} ₽ (сумма)",
"unselectedSubscriptionDeleted": "Невыбранная подписка будет удалена",
"historyPreserved": "История операций сохранится",
"confirm": "Объединить аккаунты",
"cancel": "Отмена",
"expired": "Время на объединение истекло. Попробуйте снова.",
"success": "Аккаунты успешно объединены!",
"error": "Ошибка при объединении. Попробуйте позже.",
"expiresIn": "Действует ещё {{minutes}}",
"merging": "Объединение..."
}
}

View File

@@ -3033,6 +3033,29 @@
"promoOffers": "促销活动",
"promoOffersDesc": "接收特别优惠和折扣",
"unavailable": "通知设置不可用"
},
"accounts": {
"title": "关联账户",
"subtitle": "管理您的登录方式",
"linked": "已关联",
"notLinked": "未关联",
"link": "关联",
"unlink": "取消关联",
"unlinkConfirm": "确定吗?取消关联后,您将无法通过此服务登录。",
"cannotUnlinkLast": "无法取消最后一种登录方式",
"linkSuccess": "账户关联成功",
"unlinkSuccess": "账户取消关联成功",
"linkError": "关联账户失败",
"unlinkError": "取消关联失败",
"goToAccounts": "关联账户",
"providers": {
"telegram": "Telegram",
"email": "邮箱",
"google": "Google",
"yandex": "Yandex",
"discord": "Discord",
"vk": "VK"
}
}
},
"theme": {
@@ -3357,5 +3380,32 @@
"requests": "请求数",
"ipHistory": "IP历史"
}
},
"merge": {
"title": "合并账户",
"description": "此登录方式已关联到另一个账户。您可以将两个账户合并为一个。",
"currentAccount": "您的当前账户",
"foundAccount": "发现的账户",
"authMethods": "登录方式",
"subscription": "订阅",
"noSubscription": "无订阅",
"traffic": "流量",
"devices": "设备",
"balance": "余额",
"until": "至 {{date}}",
"keepThisSubscription": "保留此订阅",
"chooseSubscription": "选择要保留的订阅",
"afterMerge": "合并后",
"allAuthMethodsMerged": "所有登录方式将合并",
"balanceSummed": "余额:{{amount}} ₽(合计)",
"unselectedSubscriptionDeleted": "未选择的订阅将被删除",
"historyPreserved": "交易记录将保留",
"confirm": "合并账户",
"cancel": "取消",
"expired": "合并链接已过期,请重试。",
"success": "账户合并成功!",
"error": "合并失败,请稍后重试。",
"expiresIn": "剩余 {{minutes}} 分钟",
"merging": "合并中..."
}
}