From b05407ffdd56a8d85a84f19a0c8f3489743072af Mon Sep 17 00:00:00 2001 From: Chenlei Hu Date: Thu, 24 Apr 2025 20:45:30 -0400 Subject: [PATCH] [Refactor] Split authStore into authStore and authService (#3612) Co-authored-by: github-actions --- .../dialog/content/SettingDialogContent.vue | 6 +- .../content/TopUpCreditsDialogContent.vue | 21 ++--- .../dialog/content/setting/CreditsPanel.vue | 12 +-- .../dialog/content/signin/SignInForm.vue | 19 +--- src/composables/useCoreCommands.ts | 16 +--- src/locales/en/main.json | 15 +++- src/locales/es/main.json | 11 ++- src/locales/fr/main.json | 11 ++- src/locales/ja/main.json | 11 ++- src/locales/ko/main.json | 11 ++- src/locales/ru/main.json | 11 ++- src/locales/zh/main.json | 11 ++- src/services/firebaseAuthService.ts | 90 +++++++++++++++++++ src/stores/firebaseAuthStore.ts | 86 +++++++++--------- .../tests/store/firebaseAuthStore.test.ts | 15 ---- 15 files changed, 217 insertions(+), 129 deletions(-) create mode 100644 src/services/firebaseAuthService.ts diff --git a/src/components/dialog/content/SettingDialogContent.vue b/src/components/dialog/content/SettingDialogContent.vue index 4563e22c3..b6e0e504c 100644 --- a/src/components/dialog/content/SettingDialogContent.vue +++ b/src/components/dialog/content/SettingDialogContent.vue @@ -69,7 +69,7 @@ import { computed, watch } from 'vue' import SearchBox from '@/components/common/SearchBox.vue' import { useSettingSearch } from '@/composables/setting/useSettingSearch' import { useSettingUI } from '@/composables/setting/useSettingUI' -import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore' +import { useFirebaseAuthService } from '@/services/firebaseAuthService' import { SettingTreeNode } from '@/stores/settingStore' import { ISettingGroup, SettingParams } from '@/types/settingTypes' import { flattenTree } from '@/utils/treeUtil' @@ -107,7 +107,7 @@ const { getSearchResults } = useSettingSearch() -const authStore = useFirebaseAuthStore() +const authService = useFirebaseAuthService() // Sort groups for a category const sortedGroups = (category: SettingTreeNode): ISettingGroup[] => { @@ -140,7 +140,7 @@ watch(activeCategory, (_, oldValue) => { activeCategory.value = oldValue } if (activeCategory.value?.key === 'credits') { - void authStore.fetchBalance() + void authService.fetchBalance() } }) diff --git a/src/components/dialog/content/TopUpCreditsDialogContent.vue b/src/components/dialog/content/TopUpCreditsDialogContent.vue index 6b2770d9b..d836874a6 100644 --- a/src/components/dialog/content/TopUpCreditsDialogContent.vue +++ b/src/components/dialog/content/TopUpCreditsDialogContent.vue @@ -109,8 +109,9 @@ import ProgressSpinner from 'primevue/progressspinner' import Tag from 'primevue/tag' import { computed, onBeforeUnmount, ref } from 'vue' +import { useFirebaseAuthService } from '@/services/firebaseAuthService' import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore' -import { formatMetronomeCurrency, usdToMicros } from '@/utils/formatUtil' +import { formatMetronomeCurrency } from '@/utils/formatUtil' const { isInsufficientCredits = false, @@ -123,6 +124,7 @@ const { }>() const authStore = useFirebaseAuthStore() +const authService = useFirebaseAuthService() const customAmount = ref(100) const didClickBuyNow = ref(false) const loading = computed(() => authStore.loading) @@ -133,29 +135,18 @@ const formattedBalance = computed(() => { }) const handleSeeDetails = async () => { - const response = await authStore.accessBillingPortal() - if (!response?.billing_portal_url) return - window.open(response.billing_portal_url, '_blank') + await authService.accessBillingPortal() } const handleBuyNow = async (amount: number) => { - const response = await authStore.initiateCreditPurchase({ - amount_micros: usdToMicros(amount), - currency: 'usd' - }) - - if (!response?.checkout_url) return - + await authService.purchaseCredits(amount) didClickBuyNow.value = true - - // Go to Stripe checkout page - window.open(response.checkout_url, '_blank') } onBeforeUnmount(() => { if (didClickBuyNow.value) { // If clicked buy now, then returned back to the dialog and closed, fetch the balance - void authStore.fetchBalance() + void authService.fetchBalance() } }) diff --git a/src/components/dialog/content/setting/CreditsPanel.vue b/src/components/dialog/content/setting/CreditsPanel.vue index c7ae572ab..b8af03ab4 100644 --- a/src/components/dialog/content/setting/CreditsPanel.vue +++ b/src/components/dialog/content/setting/CreditsPanel.vue @@ -51,7 +51,7 @@ text size="small" severity="secondary" - @click="() => authStore.fetchBalance()" + @click="() => authService.fetchBalance()" /> @@ -128,6 +128,7 @@ import { computed, ref } from 'vue' import { useI18n } from 'vue-i18n' import { useDialogService } from '@/services/dialogService' +import { useFirebaseAuthService } from '@/services/firebaseAuthService' import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore' import { formatMetronomeCurrency } from '@/utils/formatUtil' @@ -141,6 +142,7 @@ interface CreditHistoryItemData { const { t } = useI18n() const dialogService = useDialogService() const authStore = useFirebaseAuthStore() +const authService = useFirebaseAuthService() const loading = computed(() => authStore.loading) const balanceLoading = computed(() => authStore.isFetchingBalance) const formattedBalance = computed(() => { @@ -159,13 +161,7 @@ const handlePurchaseCreditsClick = () => { } const handleCreditsHistoryClick = async () => { - const response = await authStore.accessBillingPortal() - if (!response) return - - const { billing_portal_url } = response - if (billing_portal_url) { - window.open(billing_portal_url, '_blank') - } + await authService.accessBillingPortal() } const handleMessageSupport = () => { diff --git a/src/components/dialog/content/signin/SignInForm.vue b/src/components/dialog/content/signin/SignInForm.vue index e1fa79652..4881970fd 100644 --- a/src/components/dialog/content/signin/SignInForm.vue +++ b/src/components/dialog/content/signin/SignInForm.vue @@ -81,14 +81,14 @@ import { computed } from 'vue' import { useI18n } from 'vue-i18n' import { type SignInData, signInSchema } from '@/schemas/signInSchema' +import { useFirebaseAuthService } from '@/services/firebaseAuthService' import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore' -import { useToastStore } from '@/stores/toastStore' const authStore = useFirebaseAuthStore() +const firebaseAuthService = useFirebaseAuthService() const loading = computed(() => authStore.loading) const { t } = useI18n() -const toast = useToastStore() const emit = defineEmits<{ submit: [values: SignInData] @@ -102,19 +102,6 @@ const onSubmit = (event: FormSubmitEvent) => { const handleForgotPassword = async (email: string) => { if (!email) return - await authStore.sendPasswordReset(email) - if (authStore.error) { - toast.add({ - severity: 'error', - summary: t('auth.login.forgotPasswordError'), - detail: authStore.error - }) - } else { - toast.add({ - severity: 'success', - summary: t('auth.login.passwordResetSent'), - detail: t('auth.login.passwordResetSentDetail') - }) - } + await firebaseAuthService.sendPasswordReset(email) } diff --git a/src/composables/useCoreCommands.ts b/src/composables/useCoreCommands.ts index 30af7388a..080f62cf5 100644 --- a/src/composables/useCoreCommands.ts +++ b/src/composables/useCoreCommands.ts @@ -13,10 +13,10 @@ import { t } from '@/i18n' import { api } from '@/scripts/api' import { app } from '@/scripts/app' import { useDialogService } from '@/services/dialogService' +import { useFirebaseAuthService } from '@/services/firebaseAuthService' import { useLitegraphService } from '@/services/litegraphService' import { useWorkflowService } from '@/services/workflowService' import type { ComfyCommand } from '@/stores/commandStore' -import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore' import { useTitleEditorStore } from '@/stores/graphStore' import { useQueueSettingsStore, useQueueStore } from '@/stores/queueStore' import { useSettingStore } from '@/stores/settingStore' @@ -32,7 +32,7 @@ export function useCoreCommands(): ComfyCommand[] { const workflowStore = useWorkflowStore() const dialogService = useDialogService() const colorPaletteStore = useColorPaletteStore() - const authStore = useFirebaseAuthStore() + const firebaseAuthService = useFirebaseAuthService() const toastStore = useToastStore() const getTracker = () => workflowStore.activeWorkflow?.changeTracker @@ -649,17 +649,7 @@ export function useCoreCommands(): ComfyCommand[] { label: 'Sign Out', versionAdded: '1.18.1', function: async () => { - await authStore.logout() - if (authStore.error) { - toastStore.addAlert(authStore.error) - } else { - toastStore.add({ - severity: 'success', - summary: t('auth.signOut.success'), - detail: t('auth.signOut.successDetail'), - life: 5000 - }) - } + await firebaseAuthService.logout() } } ] diff --git a/src/locales/en/main.json b/src/locales/en/main.json index 3345a58eb..a58bf2df5 100644 --- a/src/locales/en/main.json +++ b/src/locales/en/main.json @@ -111,7 +111,8 @@ "updateAvailable": "Update Available", "login": "Login", "learnMore": "Learn more", - "amount": "Amount" + "amount": "Amount", + "unknownError": "Unknown error" }, "manager": { "title": "Custom Nodes Manager", @@ -1077,7 +1078,14 @@ "errorCopyImage": "Error copying image: {error}", "noTemplatesToExport": "No templates to export", "failedToFetchLogs": "Failed to fetch server logs", - "migrateToLitegraphReroute": "Reroute nodes will be removed in future versions. Click to migrate to litegraph-native reroute." + "migrateToLitegraphReroute": "Reroute nodes will be removed in future versions. Click to migrate to litegraph-native reroute.", + "userNotAuthenticated": "User not authenticated", + "firebaseAuthNotInitialized": "Firebase Auth not initialized", + "failedToFetchBalance": "Failed to fetch balance: {error}", + "failedToCreateCustomer": "Failed to create customer: {error}", + "failedToInitiateCreditPurchase": "Failed to initiate credit purchase: {error}", + "failedToAccessBillingPortal": "Failed to access billing portal: {error}", + "failedToPurchaseCredits": "Failed to purchase credits: {error}" }, "auth": { "login": { @@ -1104,8 +1112,7 @@ "andText": "and", "privacyLink": "Privacy Policy", "success": "Login successful", - "failed": "Login failed", - "genericErrorMessage": "Sorry, we've encountered an error. Please contact {supportEmail}." + "failed": "Login failed" }, "signup": { "title": "Create an account", diff --git a/src/locales/es/main.json b/src/locales/es/main.json index 55bc9c0e0..6b21a1691 100644 --- a/src/locales/es/main.json +++ b/src/locales/es/main.json @@ -18,7 +18,6 @@ "failed": "Inicio de sesión fallido", "forgotPassword": "¿Olvidaste tu contraseña?", "forgotPasswordError": "No se pudo enviar el correo electrónico para restablecer la contraseña", - "genericErrorMessage": "Lo sentimos, hemos encontrado un error. Por favor, contacta con {supportEmail}.", "loginButton": "Iniciar sesión", "loginWithGithub": "Iniciar sesión con Github", "loginWithGoogle": "Iniciar sesión con Google", @@ -297,6 +296,7 @@ "success": "Éxito", "systemInfo": "Información del sistema", "terminal": "Terminal", + "unknownError": "Error desconocido", "update": "Actualizar", "updateAvailable": "Actualización Disponible", "updated": "Actualizado", @@ -1108,12 +1108,18 @@ "errorCopyImage": "Error al copiar la imagen: {error}", "errorLoadingModel": "Error al cargar el modelo", "errorSaveSetting": "Error al guardar la configuración {id}: {err}", + "failedToAccessBillingPortal": "No se pudo acceder al portal de facturación: {error}", "failedToApplyTexture": "Error al aplicar textura", + "failedToCreateCustomer": "No se pudo crear el cliente: {error}", "failedToDownloadFile": "Error al descargar el archivo", "failedToExportModel": "Error al exportar modelo como {format}", + "failedToFetchBalance": "No se pudo obtener el saldo: {error}", "failedToFetchLogs": "Error al obtener los registros del servidor", + "failedToInitiateCreditPurchase": "No se pudo iniciar la compra de créditos: {error}", + "failedToPurchaseCredits": "No se pudo comprar créditos: {error}", "fileLoadError": "No se puede encontrar el flujo de trabajo en {fileName}", "fileUploadFailed": "Error al subir el archivo", + "firebaseAuthNotInitialized": "Firebase Auth no está inicializado", "interrupted": "La ejecución ha sido interrumpida", "migrateToLitegraphReroute": "Los nodos de reroute se eliminarán en futuras versiones. Haz clic para migrar a reroute nativo de litegraph.", "no3dScene": "No hay escena 3D para aplicar textura", @@ -1124,7 +1130,8 @@ "pendingTasksDeleted": "Tareas pendientes eliminadas", "pleaseSelectNodesToGroup": "Por favor, seleccione los nodos (u otros grupos) para crear un grupo para", "unableToGetModelFilePath": "No se puede obtener la ruta del archivo del modelo", - "updateRequested": "Actualización solicitada" + "updateRequested": "Actualización solicitada", + "userNotAuthenticated": "Usuario no autenticado" }, "userSelect": { "enterUsername": "Introduce un nombre de usuario", diff --git a/src/locales/fr/main.json b/src/locales/fr/main.json index fd7dfeb2d..c24eea2c5 100644 --- a/src/locales/fr/main.json +++ b/src/locales/fr/main.json @@ -18,7 +18,6 @@ "failed": "Échec de la connexion", "forgotPassword": "Mot de passe oublié?", "forgotPasswordError": "Échec de l'envoi de l'e-mail de réinitialisation du mot de passe", - "genericErrorMessage": "Désolé, une erreur s'est produite. Veuillez contacter {supportEmail}.", "loginButton": "Se connecter", "loginWithGithub": "Se connecter avec Github", "loginWithGoogle": "Se connecter avec Google", @@ -297,6 +296,7 @@ "success": "Succès", "systemInfo": "Informations système", "terminal": "Terminal", + "unknownError": "Erreur inconnue", "update": "Mettre à jour", "updateAvailable": "Mise à jour disponible", "updated": "Mis à jour", @@ -1108,12 +1108,18 @@ "errorCopyImage": "Erreur lors de la copie de l'image: {error}", "errorLoadingModel": "Erreur lors du chargement du modèle", "errorSaveSetting": "Erreur lors de l'enregistrement du paramètre {id}: {err}", + "failedToAccessBillingPortal": "Échec de l'accès au portail de facturation : {error}", "failedToApplyTexture": "Échec de l'application de la texture", + "failedToCreateCustomer": "Échec de la création du client : {error}", "failedToDownloadFile": "Échec du téléchargement du fichier", "failedToExportModel": "Échec de l'exportation du modèle en {format}", + "failedToFetchBalance": "Échec de la récupération du solde : {error}", "failedToFetchLogs": "Échec de la récupération des journaux du serveur", + "failedToInitiateCreditPurchase": "Échec de l'initiation de l'achat de crédits : {error}", + "failedToPurchaseCredits": "Échec de l'achat de crédits : {error}", "fileLoadError": "Impossible de trouver le flux de travail dans {fileName}", "fileUploadFailed": "Échec du téléchargement du fichier", + "firebaseAuthNotInitialized": "Authentification Firebase non initialisée", "interrupted": "L'exécution a été interrompue", "migrateToLitegraphReroute": "Les nœuds de reroute seront supprimés dans les futures versions. Cliquez pour migrer vers le reroute natif de litegraph.", "no3dScene": "Aucune scène 3D pour appliquer la texture", @@ -1124,7 +1130,8 @@ "pendingTasksDeleted": "Tâches en attente supprimées", "pleaseSelectNodesToGroup": "Veuillez sélectionner les nœuds (ou autres groupes) pour créer un groupe pour", "unableToGetModelFilePath": "Impossible d'obtenir le chemin du fichier modèle", - "updateRequested": "Mise à jour demandée" + "updateRequested": "Mise à jour demandée", + "userNotAuthenticated": "Utilisateur non authentifié" }, "userSelect": { "enterUsername": "Entrez un nom d'utilisateur", diff --git a/src/locales/ja/main.json b/src/locales/ja/main.json index b507db003..3d3002c1f 100644 --- a/src/locales/ja/main.json +++ b/src/locales/ja/main.json @@ -18,7 +18,6 @@ "failed": "ログイン失敗", "forgotPassword": "パスワードを忘れましたか?", "forgotPasswordError": "パスワードリセット用メールの送信に失敗しました", - "genericErrorMessage": "申し訳ありませんが、エラーが発生しました。{supportEmail} までご連絡ください。", "loginButton": "ログイン", "loginWithGithub": "Githubでログイン", "loginWithGoogle": "Googleでログイン", @@ -297,6 +296,7 @@ "success": "成功", "systemInfo": "システム情報", "terminal": "ターミナル", + "unknownError": "不明なエラー", "update": "更新", "updateAvailable": "更新が利用可能", "updated": "更新済み", @@ -1108,12 +1108,18 @@ "errorCopyImage": "画像のコピーにエラーが発生しました: {error}", "errorLoadingModel": "モデルの読み込みエラー", "errorSaveSetting": "設定{id}の保存エラー: {err}", + "failedToAccessBillingPortal": "請求ポータルへのアクセスに失敗しました: {error}", "failedToApplyTexture": "テクスチャの適用に失敗しました", + "failedToCreateCustomer": "顧客の作成に失敗しました: {error}", "failedToDownloadFile": "ファイルのダウンロードに失敗しました", "failedToExportModel": "{format}としてモデルのエクスポートに失敗しました", + "failedToFetchBalance": "残高の取得に失敗しました: {error}", "failedToFetchLogs": "サーバーログの取得に失敗しました", + "failedToInitiateCreditPurchase": "クレジット購入の開始に失敗しました: {error}", + "failedToPurchaseCredits": "クレジットの購入に失敗しました: {error}", "fileLoadError": "{fileName}でワークフローが見つかりません", "fileUploadFailed": "ファイルのアップロードに失敗しました", + "firebaseAuthNotInitialized": "Firebase認証が初期化されていません", "interrupted": "実行が中断されました", "migrateToLitegraphReroute": "将来のバージョンではRerouteノードが削除されます。litegraph-native rerouteに移行するにはクリックしてください。", "no3dScene": "テクスチャを適用する3Dシーンがありません", @@ -1124,7 +1130,8 @@ "pendingTasksDeleted": "保留中のタスクが削除されました", "pleaseSelectNodesToGroup": "グループを作成するためのノード(または他のグループ)を選択してください", "unableToGetModelFilePath": "モデルファイルのパスを取得できません", - "updateRequested": "更新が要求されました" + "updateRequested": "更新が要求されました", + "userNotAuthenticated": "ユーザーが認証されていません" }, "userSelect": { "enterUsername": "ユーザー名を入力してください", diff --git a/src/locales/ko/main.json b/src/locales/ko/main.json index 931c74c39..ae369ad64 100644 --- a/src/locales/ko/main.json +++ b/src/locales/ko/main.json @@ -18,7 +18,6 @@ "failed": "로그인 실패", "forgotPassword": "비밀번호를 잊으셨나요?", "forgotPasswordError": "비밀번호 재설정 이메일 전송에 실패했습니다", - "genericErrorMessage": "죄송합니다. 오류가 발생했습니다. {supportEmail}로 문의해 주세요.", "loginButton": "로그인", "loginWithGithub": "Github로 로그인", "loginWithGoogle": "구글로 로그인", @@ -297,6 +296,7 @@ "success": "성공", "systemInfo": "시스템 정보", "terminal": "터미널", + "unknownError": "알 수 없는 오류", "update": "업데이트", "updateAvailable": "업데이트 가능", "updated": "업데이트 됨", @@ -1108,12 +1108,18 @@ "errorCopyImage": "이미지 복사 오류: {error}", "errorLoadingModel": "모델 로딩 오류", "errorSaveSetting": "설정 {id} 저장 오류: {err}", + "failedToAccessBillingPortal": "결제 포털에 접근하지 못했습니다: {error}", "failedToApplyTexture": "텍스처 적용에 실패했습니다", + "failedToCreateCustomer": "고객 생성에 실패했습니다: {error}", "failedToDownloadFile": "파일 다운로드에 실패했습니다", "failedToExportModel": "{format} 형식으로 모델 내보내기에 실패했습니다", + "failedToFetchBalance": "잔액을 가져오지 못했습니다: {error}", "failedToFetchLogs": "서버 로그를 가져오는 데 실패했습니다", + "failedToInitiateCreditPurchase": "크레딧 구매를 시작하지 못했습니다: {error}", + "failedToPurchaseCredits": "크레딧 구매에 실패했습니다: {error}", "fileLoadError": "{fileName}에서 워크플로우를 찾을 수 없습니다", "fileUploadFailed": "파일 업로드에 실패했습니다", + "firebaseAuthNotInitialized": "Firebase 인증이 초기화되지 않았습니다", "interrupted": "실행이 중단되었습니다", "migrateToLitegraphReroute": "향후 버전에서는 Reroute 노드가 제거됩니다. LiteGraph 에서 자체 제공하는 경유점으로 변환하려면 클릭하세요.", "no3dScene": "텍스처를 적용할 3D 장면이 없습니다", @@ -1124,7 +1130,8 @@ "pendingTasksDeleted": "보류 중인 작업이 삭제되었습니다", "pleaseSelectNodesToGroup": "그룹을 만들기 위해 노드(또는 다른 그룹)를 선택해 주세요", "unableToGetModelFilePath": "모델 파일 경로를 가져올 수 없습니다", - "updateRequested": "업데이트 요청됨" + "updateRequested": "업데이트 요청됨", + "userNotAuthenticated": "사용자가 인증되지 않았습니다" }, "userSelect": { "enterUsername": "사용자 이름 입력", diff --git a/src/locales/ru/main.json b/src/locales/ru/main.json index 2ae184dfa..7c4d71321 100644 --- a/src/locales/ru/main.json +++ b/src/locales/ru/main.json @@ -18,7 +18,6 @@ "failed": "Вход не удался", "forgotPassword": "Забыли пароль?", "forgotPasswordError": "Не удалось отправить письмо для сброса пароля", - "genericErrorMessage": "Извините, произошла ошибка. Пожалуйста, свяжитесь с {supportEmail}.", "loginButton": "Войти", "loginWithGithub": "Войти через Github", "loginWithGoogle": "Войти через Google", @@ -297,6 +296,7 @@ "success": "Успех", "systemInfo": "Информация о системе", "terminal": "Терминал", + "unknownError": "Неизвестная ошибка", "update": "Обновить", "updateAvailable": "Доступно обновление", "updated": "Обновлено", @@ -1108,12 +1108,18 @@ "errorCopyImage": "Ошибка копирования изображения: {error}", "errorLoadingModel": "Ошибка загрузки модели", "errorSaveSetting": "Ошибка сохранения настройки {id}: {err}", + "failedToAccessBillingPortal": "Не удалось получить доступ к биллинговому порталу: {error}", "failedToApplyTexture": "Не удалось применить текстуру", + "failedToCreateCustomer": "Не удалось создать клиента: {error}", "failedToDownloadFile": "Не удалось скачать файл", "failedToExportModel": "Не удалось экспортировать модель как {format}", + "failedToFetchBalance": "Не удалось получить баланс: {error}", "failedToFetchLogs": "Не удалось получить серверные логи", + "failedToInitiateCreditPurchase": "Не удалось начать покупку кредитов: {error}", + "failedToPurchaseCredits": "Не удалось купить кредиты: {error}", "fileLoadError": "Не удалось найти рабочий процесс в {fileName}", "fileUploadFailed": "Не удалось загрузить файл", + "firebaseAuthNotInitialized": "Firebase Auth не инициализирован", "interrupted": "Выполнение было прервано", "migrateToLitegraphReroute": "Узлы перенаправления будут удалены в будущих версиях. Нажмите, чтобы перейти на litegraph-native reroute.", "no3dScene": "Нет 3D сцены для применения текстуры", @@ -1124,7 +1130,8 @@ "pendingTasksDeleted": "Ожидающие задачи удалены", "pleaseSelectNodesToGroup": "Пожалуйста, выберите узлы (или другие группы) для создания группы", "unableToGetModelFilePath": "Не удалось получить путь к файлу модели", - "updateRequested": "Запрошено обновление" + "updateRequested": "Запрошено обновление", + "userNotAuthenticated": "Пользователь не аутентифицирован" }, "userSelect": { "enterUsername": "Введите имя пользователя", diff --git a/src/locales/zh/main.json b/src/locales/zh/main.json index 1569436a1..fb63e8bcb 100644 --- a/src/locales/zh/main.json +++ b/src/locales/zh/main.json @@ -18,7 +18,6 @@ "failed": "登录失败", "forgotPassword": "忘记密码?", "forgotPasswordError": "发送重置密码邮件失败", - "genericErrorMessage": "抱歉,我们遇到了一些错误。请联系 {supportEmail}。", "loginButton": "登录", "loginWithGithub": "使用Github登录", "loginWithGoogle": "使用Google登录", @@ -297,6 +296,7 @@ "success": "成功", "systemInfo": "系统信息", "terminal": "终端", + "unknownError": "未知错误", "update": "更新", "updateAvailable": "有更新可用", "updated": "已更新", @@ -1108,12 +1108,18 @@ "errorCopyImage": "复制图片出错:{error}", "errorLoadingModel": "加载模型出错", "errorSaveSetting": "保存设置 {id} 出错:{err}", + "failedToAccessBillingPortal": "访问账单门户失败:{error}", "failedToApplyTexture": "应用纹理失败", + "failedToCreateCustomer": "创建客户失败:{error}", "failedToDownloadFile": "文件下载失败", "failedToExportModel": "无法将模型导出为 {format}", + "failedToFetchBalance": "获取余额失败:{error}", "failedToFetchLogs": "无法获取服务器日志", + "failedToInitiateCreditPurchase": "发起积分购买失败:{error}", + "failedToPurchaseCredits": "购买积分失败:{error}", "fileLoadError": "无法在 {fileName} 中找到工作流", "fileUploadFailed": "文件上传失败", + "firebaseAuthNotInitialized": "Firebase 认证未初始化", "interrupted": "执行已被中断", "migrateToLitegraphReroute": "将来的版本中将删除重定向节点。点击以迁移到litegraph-native重定向。", "no3dScene": "没有3D场景可以应用纹理", @@ -1124,7 +1130,8 @@ "pendingTasksDeleted": "待处理任务已删除", "pleaseSelectNodesToGroup": "请选取节点(或其他组)以创建分组", "unableToGetModelFilePath": "无法获取模型文件路径", - "updateRequested": "已请求更新" + "updateRequested": "已请求更新", + "userNotAuthenticated": "用户未认证" }, "userSelect": { "enterUsername": "输入用户名", diff --git a/src/services/firebaseAuthService.ts b/src/services/firebaseAuthService.ts new file mode 100644 index 000000000..a63c52f15 --- /dev/null +++ b/src/services/firebaseAuthService.ts @@ -0,0 +1,90 @@ +import { useErrorHandling } from '@/composables/useErrorHandling' +import { t } from '@/i18n' +import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore' +import { useToastStore } from '@/stores/toastStore' +import { usdToMicros } from '@/utils/formatUtil' + +/** + * Service for Firebase Auth actions. + * All actions are wrapped with error handling. + * @returns {Object} - Object containing all Firebase Auth actions + */ +export const useFirebaseAuthService = () => { + const authStore = useFirebaseAuthStore() + const toastStore = useToastStore() + const { wrapWithErrorHandlingAsync } = useErrorHandling() + + const reportError = (error: unknown) => { + toastStore.add({ + severity: 'error', + summary: t('g.error'), + detail: error instanceof Error ? error.message : t('g.unknownError'), + life: 5000 + }) + } + + const logout = wrapWithErrorHandlingAsync(async () => { + await authStore.logout() + toastStore.add({ + severity: 'success', + summary: t('auth.signOut.success'), + detail: t('auth.signOut.successDetail'), + life: 5000 + }) + }, reportError) + + const sendPasswordReset = wrapWithErrorHandlingAsync( + async (email: string) => { + await authStore.sendPasswordReset(email) + toastStore.add({ + severity: 'success', + summary: t('auth.login.passwordResetSent'), + detail: t('auth.login.passwordResetSentDetail'), + life: 5000 + }) + }, + reportError + ) + + const purchaseCredits = wrapWithErrorHandlingAsync(async (amount: number) => { + const response = await authStore.initiateCreditPurchase({ + amount_micros: usdToMicros(amount), + currency: 'usd' + }) + + if (!response.checkout_url) { + throw new Error( + t('toastMessages.failedToPurchaseCredits', { + error: 'No checkout URL returned' + }) + ) + } + + // Go to Stripe checkout page + window.open(response.checkout_url, '_blank') + }, reportError) + + const accessBillingPortal = wrapWithErrorHandlingAsync(async () => { + const response = await authStore.accessBillingPortal() + if (!response.billing_portal_url) { + throw new Error( + t('toastMessages.failedToAccessBillingPortal', { + error: 'No billing portal URL returned' + }) + ) + } + window.open(response.billing_portal_url, '_blank') + }, reportError) + + const fetchBalance = wrapWithErrorHandlingAsync(async () => { + await authStore.fetchBalance() + }, reportError) + + return { + logout, + sendPasswordReset, + purchaseCredits, + accessBillingPortal, + fetchBalance + } +} diff --git a/src/stores/firebaseAuthStore.ts b/src/stores/firebaseAuthStore.ts index 9476fd71f..9f9a905cb 100644 --- a/src/stores/firebaseAuthStore.ts +++ b/src/stores/firebaseAuthStore.ts @@ -21,8 +21,6 @@ import { COMFY_API_BASE_URL } from '@/config/comfyApi' import { t } from '@/i18n' import { operations } from '@/types/comfyRegistryTypes' -import { useToastStore } from './toastStore' - type CreditPurchaseResponse = operations['InitiateCreditPurchase']['responses']['201']['content']['application/json'] type CreditPurchasePayload = @@ -36,10 +34,16 @@ type AccessBillingPortalResponse = type AccessBillingPortalReqBody = operations['AccessBillingPortal']['requestBody'] +export class FirebaseAuthStoreError extends Error { + constructor(message: string) { + super(message) + this.name = 'FirebaseAuthStoreError' + } +} + export const useFirebaseAuthStore = defineStore('firebaseAuth', () => { // State const loading = ref(false) - const error = ref(null) const currentUser = ref(null) const isInitialized = ref(false) const customerCreated = ref(false) @@ -75,16 +79,6 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => { lastBalanceUpdateTime.value = null }) - const showAuthErrorToast = () => { - useToastStore().add({ - summary: t('g.error'), - detail: t('auth.login.genericErrorMessage', { - supportEmail: 'support@comfy.org' - }), - severity: 'error' - }) - } - const getIdToken = async (): Promise => { if (currentUser.value) { return currentUser.value.getIdToken() @@ -97,9 +91,10 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => { try { const token = await getIdToken() if (!token) { - error.value = 'Cannot fetch balance: User not authenticated' isFetchingBalance.value = false - return null + throw new FirebaseAuthStoreError( + t('toastMessages.userNotAuthenticated') + ) } const response = await fetch(`${COMFY_API_BASE_URL}/customers/balance`, { @@ -114,8 +109,11 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => { return null } const errorData = await response.json() - error.value = `Failed to fetch balance: ${errorData.message}` - return null + throw new FirebaseAuthStoreError( + t('toastMessages.failedToFetchBalance', { + error: errorData.message + }) + ) } const balanceData = await response.json() @@ -123,9 +121,6 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => { lastBalanceUpdateTime.value = new Date() balance.value = balanceData return balanceData - } catch (e) { - error.value = `Failed to fetch balance: ${e}` - return null } finally { isFetchingBalance.value = false } @@ -142,15 +137,21 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => { } }) if (!createCustomerRes.ok) { - throw new Error( - `Failed to create customer: ${createCustomerRes.statusText}` + throw new FirebaseAuthStoreError( + t('toastMessages.failedToCreateCustomer', { + error: createCustomerRes.statusText + }) ) } const createCustomerResJson: CreateCustomerResponse = await createCustomerRes.json() if (!createCustomerResJson?.id) { - throw new Error('Failed to create customer: No customer ID returned') + throw new FirebaseAuthStoreError( + t('toastMessages.failedToCreateCustomer', { + error: 'No customer ID returned' + }) + ) } return createCustomerResJson @@ -162,10 +163,12 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => { createCustomer?: boolean } = {} ): Promise => { - if (!auth) throw new Error('Firebase Auth not initialized') + if (!auth) + throw new FirebaseAuthStoreError( + t('toastMessages.firebaseAuthNotInitialized') + ) loading.value = true - error.value = null try { const result = await action(auth) @@ -180,10 +183,6 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => { } return result - } catch (e: unknown) { - error.value = e instanceof Error ? e.message : 'Unknown error' - showAuthErrorToast() - throw e } finally { loading.value = false } @@ -232,11 +231,10 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => { const addCredits = async ( requestBodyContent: CreditPurchasePayload - ): Promise => { + ): Promise => { const token = await getIdToken() if (!token) { - error.value = 'Cannot add credits: User not authenticated' - return null + throw new FirebaseAuthStoreError(t('toastMessages.userNotAuthenticated')) } // Ensure customer was created during login/registration @@ -256,9 +254,11 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => { if (!response.ok) { const errorData = await response.json() - error.value = `Failed to initiate credit purchase: ${errorData.message}` - showAuthErrorToast() - return null + throw new FirebaseAuthStoreError( + t('toastMessages.failedToInitiateCreditPurchase', { + error: errorData.message + }) + ) } return response.json() @@ -266,16 +266,15 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => { const initiateCreditPurchase = async ( requestBodyContent: CreditPurchasePayload - ): Promise => + ): Promise => executeAuthAction((_) => addCredits(requestBodyContent)) const accessBillingPortal = async ( requestBody?: AccessBillingPortalReqBody - ): Promise => { + ): Promise => { const token = await getIdToken() if (!token) { - error.value = 'Cannot access billing portal: User not authenticated' - return null + throw new FirebaseAuthStoreError(t('toastMessages.userNotAuthenticated')) } const response = await fetch(`${COMFY_API_BASE_URL}/customers/billing`, { @@ -291,9 +290,11 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => { if (!response.ok) { const errorData = await response.json() - error.value = `Failed to access billing portal: ${errorData.message}` - showAuthErrorToast() - return null + throw new FirebaseAuthStoreError( + t('toastMessages.failedToAccessBillingPortal', { + error: errorData.message + }) + ) } return response.json() @@ -302,7 +303,6 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => { return { // State loading, - error, currentUser, isInitialized, balance, diff --git a/tests-ui/tests/store/firebaseAuthStore.test.ts b/tests-ui/tests/store/firebaseAuthStore.test.ts index 6933dfea9..1780f801a 100644 --- a/tests-ui/tests/store/firebaseAuthStore.test.ts +++ b/tests-ui/tests/store/firebaseAuthStore.test.ts @@ -135,7 +135,6 @@ describe('useFirebaseAuthStore', () => { expect(store.userEmail).toBe('test@example.com') expect(store.userId).toBe('test-user-id') expect(store.loading).toBe(false) - expect(store.error).toBe(null) }) it('should set persistence to local storage on initialization', () => { @@ -158,17 +157,12 @@ describe('useFirebaseAuthStore', () => { // Error expected } - expect(store.error).toBe('Invalid password') - // Now, succeed on next attempt vi.mocked(firebaseAuth.signInWithEmailAndPassword).mockResolvedValueOnce({ user: mockUser } as any) await store.login('test@example.com', 'correct-password') - - // Error should be cleared - expect(store.error).toBe(null) }) describe('login', () => { @@ -187,7 +181,6 @@ describe('useFirebaseAuthStore', () => { ) expect(result).toEqual(mockUserCredential) expect(store.loading).toBe(false) - expect(store.error).toBe(null) }) it('should handle login errors', async () => { @@ -206,7 +199,6 @@ describe('useFirebaseAuthStore', () => { 'wrong-password' ) expect(store.loading).toBe(false) - expect(store.error).toBe('Invalid password') }) it('should handle concurrent login attempts correctly', async () => { @@ -243,7 +235,6 @@ describe('useFirebaseAuthStore', () => { ) expect(result).toEqual(mockUserCredential) expect(store.loading).toBe(false) - expect(store.error).toBe(null) }) it('should handle registration errors', async () => { @@ -262,7 +253,6 @@ describe('useFirebaseAuthStore', () => { 'password' ) expect(store.loading).toBe(false) - expect(store.error).toBe('Email already in use') }) }) @@ -282,7 +272,6 @@ describe('useFirebaseAuthStore', () => { await expect(store.logout()).rejects.toThrow('Network error') expect(firebaseAuth.signOut).toHaveBeenCalledWith(mockAuth) - expect(store.error).toBe('Network error') }) }) @@ -356,7 +345,6 @@ describe('useFirebaseAuthStore', () => { ) expect(result).toEqual(mockUserCredential) expect(store.loading).toBe(false) - expect(store.error).toBe(null) }) it('should handle Google sign in errors', async () => { @@ -372,7 +360,6 @@ describe('useFirebaseAuthStore', () => { expect.any(firebaseAuth.GoogleAuthProvider) ) expect(store.loading).toBe(false) - expect(store.error).toBe('Google authentication failed') }) }) @@ -391,7 +378,6 @@ describe('useFirebaseAuthStore', () => { ) expect(result).toEqual(mockUserCredential) expect(store.loading).toBe(false) - expect(store.error).toBe(null) }) it('should handle Github sign in errors', async () => { @@ -407,7 +393,6 @@ describe('useFirebaseAuthStore', () => { expect.any(firebaseAuth.GithubAuthProvider) ) expect(store.loading).toBe(false) - expect(store.error).toBe('Github authentication failed') }) })