mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-19 06:20:10 +00:00
[API Node] User management (#3567)
Co-authored-by: github-actions <github-actions@github.com> Co-authored-by: Chenlei Hu <hcl@comfy.org>
This commit is contained in:
@@ -48,6 +48,7 @@
|
||||
</PanelTemplate>
|
||||
|
||||
<AboutPanel />
|
||||
<UserPanel />
|
||||
<CreditsPanel />
|
||||
<Suspense>
|
||||
<KeybindingPanel />
|
||||
@@ -96,9 +97,16 @@ import CurrentUserMessage from './setting/CurrentUserMessage.vue'
|
||||
import FirstTimeUIMessage from './setting/FirstTimeUIMessage.vue'
|
||||
import PanelTemplate from './setting/PanelTemplate.vue'
|
||||
import SettingsPanel from './setting/SettingsPanel.vue'
|
||||
import UserPanel from './setting/UserPanel.vue'
|
||||
|
||||
const { defaultPanel } = defineProps<{
|
||||
defaultPanel?: 'about' | 'keybinding' | 'extension' | 'server-config'
|
||||
defaultPanel?:
|
||||
| 'about'
|
||||
| 'keybinding'
|
||||
| 'extension'
|
||||
| 'server-config'
|
||||
| 'user'
|
||||
| 'credits'
|
||||
}>()
|
||||
|
||||
const KeybindingPanel = defineAsyncComponent(
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
</a>
|
||||
{{ t('auth.login.andText') }}
|
||||
<a
|
||||
href="https://www.comfy.org/privacy-policy"
|
||||
href="https://www.comfy.org/privacy"
|
||||
target="_blank"
|
||||
class="text-blue-500 cursor-pointer"
|
||||
>
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<div class="flex flex-col items-center gap-4 p-4">
|
||||
<div class="flex flex-col items-center text-center">
|
||||
<i class="pi pi-exclamation-circle mb-4" style="font-size: 2rem" />
|
||||
<h2 class="text-2xl font-semibold mb-2">
|
||||
{{ $t(`auth.required.${type}.title`) }}
|
||||
</h2>
|
||||
<p class="text-gray-600 mb-4 max-w-md">
|
||||
{{ $t(`auth.required.${type}.message`) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex gap-4">
|
||||
<Button
|
||||
class="w-60"
|
||||
severity="primary"
|
||||
:label="$t(`auth.required.${type}.action`)"
|
||||
@click="openPanel"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
|
||||
const props = defineProps<{
|
||||
type: 'signIn' | 'credits'
|
||||
}>()
|
||||
|
||||
const dialogStore = useDialogStore()
|
||||
const authStore = useFirebaseAuthStore()
|
||||
|
||||
const openPanel = () => {
|
||||
// Close the current dialog
|
||||
dialogStore.closeDialog({ key: 'signin-required' })
|
||||
|
||||
// Open user settings and navigate to appropriate panel
|
||||
if (props.type === 'credits') {
|
||||
authStore.openCreditsPanel()
|
||||
} else {
|
||||
authStore.openSignInPanel()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
158
src/components/dialog/content/TopUpCreditsDialogContent.vue
Normal file
158
src/components/dialog/content/TopUpCreditsDialogContent.vue
Normal file
@@ -0,0 +1,158 @@
|
||||
<template>
|
||||
<div class="flex flex-col p-6">
|
||||
<div
|
||||
class="flex items-center gap-2"
|
||||
:class="{ 'text-red-500': isInsufficientCredits }"
|
||||
>
|
||||
<i
|
||||
:class="[
|
||||
'text-2xl',
|
||||
isInsufficientCredits ? 'pi pi-exclamation-triangle' : ''
|
||||
]"
|
||||
/>
|
||||
<h2 class="text-2xl font-semibold">
|
||||
{{
|
||||
$t(
|
||||
isInsufficientCredits
|
||||
? 'credits.topUp.insufficientTitle'
|
||||
: 'credits.topUp.title'
|
||||
)
|
||||
}}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<!-- Error Message -->
|
||||
<p v-if="isInsufficientCredits" class="text-lg text-muted mt-6">
|
||||
{{ $t('credits.topUp.insufficientMessage') }}
|
||||
</p>
|
||||
|
||||
<!-- Balance Section -->
|
||||
<div class="flex justify-between items-center mt-8">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="text-muted">{{ $t('credits.yourCreditBalance') }}</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Tag
|
||||
severity="secondary"
|
||||
icon="pi pi-dollar"
|
||||
rounded
|
||||
class="text-amber-400 p-1"
|
||||
/>
|
||||
<span class="text-2xl">{{ formattedBalance }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
text
|
||||
severity="secondary"
|
||||
:label="$t('credits.creditsHistory')"
|
||||
icon="pi pi-arrow-up-right"
|
||||
@click="handleSeeDetails"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Amount Input Section -->
|
||||
<div class="flex flex-col gap-2 mt-8">
|
||||
<div>
|
||||
<span class="text-muted">{{ $t('credits.topUp.addCredits') }}</span>
|
||||
<span class="text-muted text-sm ml-1">{{
|
||||
$t('credits.topUp.maxAmount')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Tag
|
||||
severity="secondary"
|
||||
icon="pi pi-dollar"
|
||||
rounded
|
||||
class="text-amber-400 p-1"
|
||||
/>
|
||||
<InputNumber
|
||||
v-model="amount"
|
||||
:min="1"
|
||||
:max="1000"
|
||||
:step="1"
|
||||
mode="currency"
|
||||
currency="USD"
|
||||
show-buttons
|
||||
@blur="handleBlur"
|
||||
@input="handleInput"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end mt-8">
|
||||
<ProgressSpinner v-if="loading" class="w-8 h-8" />
|
||||
<Button
|
||||
v-else
|
||||
severity="primary"
|
||||
:label="$t('credits.topUp.buyNow')"
|
||||
:disabled="!amount || amount > 1000"
|
||||
:pt="{
|
||||
root: { class: 'px-8' }
|
||||
}"
|
||||
@click="handleBuyNow"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import InputNumber from 'primevue/inputnumber'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import Tag from 'primevue/tag'
|
||||
import { computed, onBeforeUnmount, ref } from 'vue'
|
||||
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { formatMetronomeCurrency, usdToMicros } from '@/utils/formatUtil'
|
||||
|
||||
defineProps<{
|
||||
isInsufficientCredits?: boolean
|
||||
}>()
|
||||
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const amount = ref<number>(9.99)
|
||||
const didClickBuyNow = ref(false)
|
||||
const loading = computed(() => authStore.loading)
|
||||
|
||||
const handleBlur = (e: any) => {
|
||||
if (e.target.value) {
|
||||
amount.value = parseFloat(e.target.value)
|
||||
}
|
||||
}
|
||||
|
||||
const handleInput = (e: any) => {
|
||||
amount.value = e.value
|
||||
}
|
||||
|
||||
const formattedBalance = computed(() => {
|
||||
if (!authStore.balance) return '0.000'
|
||||
return formatMetronomeCurrency(authStore.balance.amount_micros, 'usd')
|
||||
})
|
||||
|
||||
const handleSeeDetails = async () => {
|
||||
const response = await authStore.accessBillingPortal()
|
||||
if (!response?.billing_portal_url) return
|
||||
window.open(response.billing_portal_url, '_blank')
|
||||
}
|
||||
|
||||
const handleBuyNow = async () => {
|
||||
if (!amount.value) return
|
||||
|
||||
const response = await authStore.initiateCreditPurchase({
|
||||
amount_micros: usdToMicros(amount.value),
|
||||
currency: 'usd'
|
||||
})
|
||||
|
||||
if (!response?.checkout_url) return
|
||||
|
||||
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()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -19,52 +19,71 @@
|
||||
rounded
|
||||
class="text-amber-400 p-1"
|
||||
/>
|
||||
<div class="text-3xl font-bold">{{ creditBalance }}</div>
|
||||
<div class="text-3xl font-bold">{{ formattedBalance }}</div>
|
||||
</div>
|
||||
<ProgressSpinner
|
||||
v-if="loading"
|
||||
class="w-12 h-12"
|
||||
style="--pc-spinner-color: #000"
|
||||
/>
|
||||
<Button
|
||||
v-else
|
||||
:label="$t('credits.purchaseCredits')"
|
||||
:loading="loading"
|
||||
@click="handlePurchaseCreditsClick"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-row items-center">
|
||||
<div v-if="formattedLastUpdateTime" class="text-xs text-muted">
|
||||
{{ $t('credits.lastUpdated') }}: {{ formattedLastUpdateTime }}
|
||||
</div>
|
||||
<Button
|
||||
:label="$t('credits.purchaseCredits')"
|
||||
:loading
|
||||
@click="handlePurchaseCreditsClick"
|
||||
icon="pi pi-refresh"
|
||||
text
|
||||
size="small"
|
||||
severity="secondary"
|
||||
@click="() => authStore.fetchBalance()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider class="mt-12" />
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="text-base font-medium">
|
||||
{{ $t('credits.creditsHistory') }}
|
||||
</h3>
|
||||
<div class="flex justify-between items-center mt-8">
|
||||
<Button
|
||||
:label="$t('credits.paymentDetails')"
|
||||
:label="$t('credits.creditsHistory')"
|
||||
text
|
||||
severity="secondary"
|
||||
icon="pi pi-arrow-up-right"
|
||||
:loading="loading"
|
||||
@click="handleCreditsHistoryClick"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex-grow">
|
||||
<DataTable :value="creditHistory" :show-headers="false">
|
||||
<Column field="title" :header="$t('g.name')">
|
||||
<template #body="{ data }">
|
||||
<div class="text-sm font-medium">{{ data.title }}</div>
|
||||
<div class="text-xs text-muted">{{ data.timestamp }}</div>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="amount" :header="$t('g.amount')">
|
||||
<template #body="{ data }">
|
||||
<div
|
||||
:class="[
|
||||
'text-base font-medium text-center',
|
||||
data.isPositive ? 'text-sky-500' : 'text-red-400'
|
||||
]"
|
||||
>
|
||||
{{ data.isPositive ? '+' : '-' }}{{ data.amount }}
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
</div>
|
||||
<template v-if="creditHistory.length > 0">
|
||||
<div class="flex-grow">
|
||||
<DataTable :value="creditHistory" :show-headers="false">
|
||||
<Column field="title" :header="$t('g.name')">
|
||||
<template #body="{ data }">
|
||||
<div class="text-sm font-medium">{{ data.title }}</div>
|
||||
<div class="text-xs text-muted">{{ data.timestamp }}</div>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="amount" :header="$t('g.amount')">
|
||||
<template #body="{ data }">
|
||||
<div
|
||||
:class="[
|
||||
'text-base font-medium text-center',
|
||||
data.isPositive ? 'text-sky-500' : 'text-red-400'
|
||||
]"
|
||||
>
|
||||
{{ data.isPositive ? '+' : '-' }}${{
|
||||
formatMetronomeCurrency(data.amount, 'usd')
|
||||
}}
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<Divider />
|
||||
|
||||
@@ -74,12 +93,14 @@
|
||||
text
|
||||
severity="secondary"
|
||||
icon="pi pi-question-circle"
|
||||
@click="handleFaqClick"
|
||||
/>
|
||||
<Button
|
||||
:label="$t('credits.messageSupport')"
|
||||
text
|
||||
severity="secondary"
|
||||
icon="pi pi-comments"
|
||||
@click="handleMessageSupport"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -91,20 +112,15 @@ import Button from 'primevue/button'
|
||||
import Column from 'primevue/column'
|
||||
import DataTable from 'primevue/datatable'
|
||||
import Divider from 'primevue/divider'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import TabPanel from 'primevue/tabpanel'
|
||||
import Tag from 'primevue/tag'
|
||||
import { ref } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { usdToMicros } from '@/utils/formatUtil'
|
||||
|
||||
// TODO: Mock data - in a real implementation, this would come from a store or API
|
||||
const creditBalance = ref(0.05)
|
||||
|
||||
// TODO: Either: (1) Get checkout URL that allows setting price on Stripe side, (2) Add number selection on credits panel
|
||||
const selectedCurrencyAmount = usdToMicros(10)
|
||||
|
||||
const selectedCurrency = 'usd' // For now, only USD is supported on comfy-api backend
|
||||
import { formatMetronomeCurrency } from '@/utils/formatUtil'
|
||||
|
||||
interface CreditHistoryItemData {
|
||||
title: string
|
||||
@@ -113,46 +129,56 @@ interface CreditHistoryItemData {
|
||||
isPositive: boolean
|
||||
}
|
||||
|
||||
const { initiateCreditPurchase, loading } = useFirebaseAuthStore()
|
||||
const { t } = useI18n()
|
||||
const dialogService = useDialogService()
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const loading = computed(() => authStore.loading)
|
||||
|
||||
const handlePurchaseCreditsClick = async () => {
|
||||
const response = await initiateCreditPurchase({
|
||||
amount_micros: selectedCurrencyAmount,
|
||||
currency: selectedCurrency
|
||||
})
|
||||
// Format balance from micros to dollars
|
||||
const formattedBalance = computed(() => {
|
||||
if (!authStore.balance) return '0.00'
|
||||
return formatMetronomeCurrency(authStore.balance.amount_micros, 'usd')
|
||||
})
|
||||
|
||||
const formattedLastUpdateTime = computed(() =>
|
||||
authStore.lastBalanceUpdateTime
|
||||
? authStore.lastBalanceUpdateTime.toLocaleString()
|
||||
: ''
|
||||
)
|
||||
|
||||
const handlePurchaseCreditsClick = () => {
|
||||
dialogService.showTopUpCreditsDialog()
|
||||
}
|
||||
|
||||
const handleCreditsHistoryClick = async () => {
|
||||
const response = await authStore.accessBillingPortal()
|
||||
if (!response) return
|
||||
|
||||
const { checkout_url } = response
|
||||
if (checkout_url !== undefined) {
|
||||
// Go to Stripe checkout page
|
||||
window.open(checkout_url, '_blank')
|
||||
const { billing_portal_url } = response
|
||||
if (billing_portal_url) {
|
||||
window.open(billing_portal_url, '_blank')
|
||||
}
|
||||
}
|
||||
|
||||
const creditHistory = ref<CreditHistoryItemData[]>([
|
||||
{
|
||||
title: 'Kling Text-to-Video v1-6',
|
||||
timestamp: '2025-04-09, 12:50:08 p.m.',
|
||||
amount: 4,
|
||||
isPositive: false
|
||||
},
|
||||
{
|
||||
title: 'Kling Text-to-Video v1-6',
|
||||
timestamp: '2025-04-09, 12:50:08 p.m.',
|
||||
amount: 23,
|
||||
isPositive: false
|
||||
},
|
||||
{
|
||||
title: 'Kling Text-to-Video v1-6',
|
||||
timestamp: '2025-04-09, 12:50:08 p.m.',
|
||||
amount: 22,
|
||||
isPositive: false
|
||||
},
|
||||
{
|
||||
title: 'Free monthly credits',
|
||||
timestamp: '2025-04-09, 12:46:08 p.m.',
|
||||
amount: 166,
|
||||
isPositive: true
|
||||
}
|
||||
])
|
||||
const handleMessageSupport = () => {
|
||||
dialogService.showIssueReportDialog({
|
||||
title: t('credits.messageSupport'),
|
||||
subtitle: t('issueReport.feedbackTitle'),
|
||||
panelProps: {
|
||||
errorType: 'BillingSupport',
|
||||
defaultFields: ['SystemStats', 'Settings']
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleFaqClick = () => {
|
||||
window.open('https://drip-art.notion.site/api-nodes-faqs', '_blank')
|
||||
}
|
||||
|
||||
// Fetch initial balance when panel is mounted
|
||||
onMounted(() => {
|
||||
void authStore.fetchBalance()
|
||||
})
|
||||
|
||||
const creditHistory = ref<CreditHistoryItemData[]>([])
|
||||
</script>
|
||||
|
||||
137
src/components/dialog/content/setting/UserPanel.vue
Normal file
137
src/components/dialog/content/setting/UserPanel.vue
Normal file
@@ -0,0 +1,137 @@
|
||||
<template>
|
||||
<TabPanel value="User" class="user-settings-container h-full">
|
||||
<div class="flex flex-col h-full">
|
||||
<h2 class="text-xl font-bold mb-2">{{ $t('userSettings.title') }}</h2>
|
||||
<Divider class="mb-3" />
|
||||
|
||||
<div v-if="user" class="flex flex-col gap-2">
|
||||
<!-- User Avatar if available -->
|
||||
<div v-if="user.photoURL" class="flex items-center gap-2">
|
||||
<img
|
||||
:src="user.photoURL"
|
||||
:alt="user.displayName || ''"
|
||||
class="w-8 h-8 rounded-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<h3 class="font-medium">
|
||||
{{ $t('userSettings.name') }}
|
||||
</h3>
|
||||
<div class="text-muted">
|
||||
{{ user.displayName || $t('userSettings.notSet') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<h3 class="font-medium">
|
||||
{{ $t('userSettings.email') }}
|
||||
</h3>
|
||||
<a :href="'mailto:' + user.email" class="hover:underline">
|
||||
{{ user.email }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<h3 class="font-medium">
|
||||
{{ $t('userSettings.provider') }}
|
||||
</h3>
|
||||
<div class="text-muted flex items-center gap-1">
|
||||
<i :class="providerIcon" />
|
||||
{{ providerName }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ProgressSpinner
|
||||
v-if="loading"
|
||||
class="w-8 h-8 mt-4"
|
||||
style="--pc-spinner-color: #000"
|
||||
/>
|
||||
<Button
|
||||
v-else
|
||||
class="mt-4 w-32"
|
||||
severity="secondary"
|
||||
:label="$t('auth.signOut.signOut')"
|
||||
icon="pi pi-sign-out"
|
||||
@click="handleSignOut"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Login Section -->
|
||||
<div v-else class="flex flex-col gap-4">
|
||||
<p class="text-gray-600">
|
||||
{{ $t('auth.login.title') }}
|
||||
</p>
|
||||
|
||||
<Button
|
||||
class="w-52"
|
||||
severity="primary"
|
||||
:loading="loading"
|
||||
:label="$t('auth.login.signInOrSignUp')"
|
||||
icon="pi pi-user"
|
||||
@click="handleSignIn"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</TabPanel>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import Divider from 'primevue/divider'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import TabPanel from 'primevue/tabpanel'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
|
||||
const toast = useToastStore()
|
||||
const { t } = useI18n()
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const dialogService = useDialogService()
|
||||
const user = computed(() => authStore.currentUser)
|
||||
const loading = computed(() => authStore.loading)
|
||||
|
||||
const providerName = computed(() => {
|
||||
const providerId = user.value?.providerData[0]?.providerId
|
||||
if (providerId?.includes('google')) {
|
||||
return 'Google'
|
||||
}
|
||||
if (providerId?.includes('github')) {
|
||||
return 'GitHub'
|
||||
}
|
||||
return providerId
|
||||
})
|
||||
|
||||
const providerIcon = computed(() => {
|
||||
const providerId = user.value?.providerData[0]?.providerId
|
||||
if (providerId?.includes('google')) {
|
||||
return 'pi pi-google'
|
||||
}
|
||||
if (providerId?.includes('github')) {
|
||||
return 'pi pi-github'
|
||||
}
|
||||
return 'pi pi-user'
|
||||
})
|
||||
|
||||
const handleSignOut = async () => {
|
||||
await authStore.logout()
|
||||
if (authStore.error) {
|
||||
toast.addAlert(authStore.error)
|
||||
} else {
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: t('auth.signOut.success'),
|
||||
detail: t('auth.signOut.successDetail'),
|
||||
life: 5000
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleSignIn = async () => {
|
||||
await dialogService.showSignInDialog()
|
||||
}
|
||||
</script>
|
||||
@@ -57,7 +57,9 @@
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<ProgressSpinner v-if="loading" class="w-8 h-8" />
|
||||
<Button
|
||||
v-else
|
||||
type="submit"
|
||||
:label="t('auth.login.loginButton')"
|
||||
class="h-10 font-medium mt-4"
|
||||
@@ -71,9 +73,15 @@ import { zodResolver } from '@primevue/forms/resolvers/zod'
|
||||
import Button from 'primevue/button'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Password from 'primevue/password'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { type SignInData, signInSchema } from '@/schemas/signInSchema'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const loading = computed(() => authStore.loading)
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
|
||||
@@ -23,6 +23,23 @@
|
||||
@click="workspaceState.focusMode = true"
|
||||
@contextmenu="showNativeSystemMenu"
|
||||
/>
|
||||
<Button
|
||||
v-if="isAuthenticated"
|
||||
v-tooltip="{ value: $t('userSettings.title'), showDelay: 300 }"
|
||||
class="flex-shrink-0 user-profile-button"
|
||||
severity="secondary"
|
||||
text
|
||||
:aria-label="$t('userSettings.title')"
|
||||
@click="openUserSettings"
|
||||
>
|
||||
<template #icon>
|
||||
<div
|
||||
class="w-6 h-6 rounded-full bg-neutral-100 dark:bg-neutral-700 flex items-center justify-center"
|
||||
>
|
||||
<i class="pi pi-user text-sm" />
|
||||
</div>
|
||||
</template>
|
||||
</Button>
|
||||
<div
|
||||
v-show="menuSetting !== 'Bottom'"
|
||||
class="window-actions-spacer flex-shrink-0"
|
||||
@@ -46,6 +63,8 @@ import BottomPanelToggleButton from '@/components/topbar/BottomPanelToggleButton
|
||||
import CommandMenubar from '@/components/topbar/CommandMenubar.vue'
|
||||
import WorkflowTabs from '@/components/topbar/WorkflowTabs.vue'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import {
|
||||
@@ -57,6 +76,10 @@ import {
|
||||
|
||||
const workspaceState = useWorkspaceStore()
|
||||
const settingStore = useSettingStore()
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const dialogService = useDialogService()
|
||||
|
||||
const isAuthenticated = computed(() => authStore.isAuthenticated)
|
||||
const workflowTabsPosition = computed(() =>
|
||||
settingStore.get('Comfy.Workflow.WorkflowTabsPosition')
|
||||
)
|
||||
@@ -66,6 +89,10 @@ const showTopMenu = computed(
|
||||
() => betaMenuEnabled.value && !workspaceState.focusMode
|
||||
)
|
||||
|
||||
const openUserSettings = () => {
|
||||
dialogService.showSettingsDialog('user')
|
||||
}
|
||||
|
||||
const menuRight = ref<HTMLDivElement | null>(null)
|
||||
// Menu-right holds legacy topbar elements attached by custom scripts
|
||||
onMounted(() => {
|
||||
|
||||
@@ -9,7 +9,13 @@ import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
import { buildTree } from '@/utils/treeUtil'
|
||||
|
||||
export function useSettingUI(
|
||||
defaultPanel?: 'about' | 'keybinding' | 'extension' | 'server-config'
|
||||
defaultPanel?:
|
||||
| 'about'
|
||||
| 'keybinding'
|
||||
| 'extension'
|
||||
| 'server-config'
|
||||
| 'user'
|
||||
| 'credits'
|
||||
) {
|
||||
const { t } = useI18n()
|
||||
const firebaseAuthStore = useFirebaseAuthStore()
|
||||
@@ -55,6 +61,12 @@ export function useSettingUI(
|
||||
children: []
|
||||
}
|
||||
|
||||
const userPanelNode: SettingTreeNode = {
|
||||
key: 'user',
|
||||
label: 'User',
|
||||
children: []
|
||||
}
|
||||
|
||||
const keybindingPanelNode: SettingTreeNode = {
|
||||
key: 'keybinding',
|
||||
label: 'Keybinding',
|
||||
@@ -84,10 +96,13 @@ export function useSettingUI(
|
||||
* The default category to show when the dialog is opened.
|
||||
*/
|
||||
const defaultCategory = computed<SettingTreeNode>(() => {
|
||||
return defaultPanel
|
||||
? settingCategories.value.find((x) => x.key === defaultPanel) ??
|
||||
settingCategories.value[0]
|
||||
: settingCategories.value[0]
|
||||
if (!defaultPanel) return settingCategories.value[0]
|
||||
// Search through all groups in groupedMenuTreeNodes
|
||||
for (const group of groupedMenuTreeNodes.value) {
|
||||
const found = group.children?.find((node) => node.key === defaultPanel)
|
||||
if (found) return found
|
||||
}
|
||||
return settingCategories.value[0]
|
||||
})
|
||||
|
||||
const translateCategory = (node: SettingTreeNode) => ({
|
||||
@@ -99,16 +114,15 @@ export function useSettingUI(
|
||||
})
|
||||
|
||||
const groupedMenuTreeNodes = computed<SettingTreeNode[]>(() => [
|
||||
// Account settings - only show when user is authenticated
|
||||
...(firebaseAuthStore.isAuthenticated
|
||||
? [
|
||||
{
|
||||
key: 'account',
|
||||
label: 'Account',
|
||||
children: [creditsPanelNode].map(translateCategory)
|
||||
}
|
||||
]
|
||||
: []),
|
||||
// Account settings - only show credits when user is authenticated
|
||||
{
|
||||
key: 'account',
|
||||
label: 'Account',
|
||||
children: [
|
||||
userPanelNode,
|
||||
...(firebaseAuthStore.isAuthenticated ? [creditsPanelNode] : [])
|
||||
].map(translateCategory)
|
||||
},
|
||||
// Normal settings stored in the settingStore
|
||||
{
|
||||
key: 'settings',
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { FirebaseOptions } from 'firebase/app'
|
||||
|
||||
const DEV_CONFIG: FirebaseOptions = {
|
||||
apiKey: 'AIzaSyDa_YMeyzV0SkVe92vBZ1tVikWBmOU5KVE',
|
||||
authDomain: 'dreamboothy-dev.firebaseapp.com',
|
||||
databaseURL: 'https://dreamboothy-dev-default-rtdb.firebaseio.com',
|
||||
projectId: 'dreamboothy-dev',
|
||||
storageBucket: 'dreamboothy-dev.appspot.com',
|
||||
messagingSenderId: '313257147182',
|
||||
appId: '1:313257147182:web:be38f6ebf74345fc7618bf',
|
||||
measurementId: 'G-YEVSMYXSPY'
|
||||
}
|
||||
// const DEV_CONFIG: FirebaseOptions = {
|
||||
// apiKey: 'AIzaSyDa_YMeyzV0SkVe92vBZ1tVikWBmOU5KVE',
|
||||
// authDomain: 'dreamboothy-dev.firebaseapp.com',
|
||||
// databaseURL: 'https://dreamboothy-dev-default-rtdb.firebaseio.com',
|
||||
// projectId: 'dreamboothy-dev',
|
||||
// storageBucket: 'dreamboothy-dev.appspot.com',
|
||||
// messagingSenderId: '313257147182',
|
||||
// appId: '1:313257147182:web:be38f6ebf74345fc7618bf',
|
||||
// measurementId: 'G-YEVSMYXSPY'
|
||||
// }
|
||||
|
||||
const PROD_CONFIG: FirebaseOptions = {
|
||||
apiKey: 'AIzaSyC2-fomLqgCjb7ELwta1I9cEarPK8ziTGs',
|
||||
@@ -26,4 +26,4 @@ const PROD_CONFIG: FirebaseOptions = {
|
||||
// Otherwise, build with `npm run build` the and set `--front-end-root` to `ComfyUI_frontend/dist`
|
||||
export const FIREBASE_CONFIG: FirebaseOptions = __USE_PROD_FIREBASE_CONFIG__
|
||||
? PROD_CONFIG
|
||||
: DEV_CONFIG
|
||||
: PROD_CONFIG // Just force prod to save time for now. change back later
|
||||
|
||||
@@ -1063,6 +1063,7 @@
|
||||
"auth": {
|
||||
"login": {
|
||||
"title": "Log in to your account",
|
||||
"signInOrSignUp": "Sign In / Sign Up",
|
||||
"newUser": "New here?",
|
||||
"signUp": "Sign up",
|
||||
"emailLabel": "Email",
|
||||
@@ -1081,7 +1082,8 @@
|
||||
"andText": "and",
|
||||
"privacyLink": "Privacy Policy",
|
||||
"success": "Login successful",
|
||||
"failed": "Login failed"
|
||||
"failed": "Login failed",
|
||||
"genericErrorMessage": "Sorry, we've encountered an error. Please contact {supportEmail}."
|
||||
},
|
||||
"signup": {
|
||||
"title": "Create an account",
|
||||
@@ -1094,6 +1096,25 @@
|
||||
"signIn": "Sign in",
|
||||
"signUpWithGoogle": "Sign up with Google",
|
||||
"signUpWithGithub": "Sign up with Github"
|
||||
},
|
||||
"signOut": {
|
||||
"signOut": "Log Out",
|
||||
"success": "Signed out successfully",
|
||||
"successDetail": "You have been signed out of your account."
|
||||
},
|
||||
"required": {
|
||||
"signIn": {
|
||||
"title": "Sign-In Required to Execute Workflow",
|
||||
"message": "This workflow includes nodes that require an active account. Please log in or create one to continue.",
|
||||
"hint": "To login go to: Settings > User > Login",
|
||||
"action": "Open Settings to Login"
|
||||
},
|
||||
"credits": {
|
||||
"title": "Credits Required to Execute Workflow",
|
||||
"message": "This workflow includes nodes that require credits. Please add credits to your account to continue.",
|
||||
"hint": "To add credits go to: Settings > User > Credits",
|
||||
"action": "Open Settings to Add Credits"
|
||||
}
|
||||
}
|
||||
},
|
||||
"validation": {
|
||||
@@ -1116,8 +1137,27 @@
|
||||
"yourCreditBalance": "Your credit balance",
|
||||
"purchaseCredits": "Purchase Credits",
|
||||
"creditsHistory": "Credits History",
|
||||
"paymentDetails": "Payment Details",
|
||||
"faqs": "FAQs",
|
||||
"messageSupport": "Message Support"
|
||||
"messageSupport": "Message Support",
|
||||
"lastUpdated": "Last updated",
|
||||
"topUp": {
|
||||
"title": "Add to Credit Balance",
|
||||
"insufficientTitle": "Insufficient Credits",
|
||||
"insufficientMessage": "You don't have enough credits to run this workflow.",
|
||||
"addCredits": "Add credits to your balance",
|
||||
"maxAmount": "(Max. $1,000 USD)",
|
||||
"buyNow": "Buy now"
|
||||
}
|
||||
},
|
||||
"userSettings": {
|
||||
"title": "User Settings",
|
||||
"name": "Name",
|
||||
"email": "Email",
|
||||
"notSet": "Not set",
|
||||
"provider": "Sign in method",
|
||||
"providers": {
|
||||
"google": "Google",
|
||||
"github": "GitHub"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
"emailPlaceholder": "Ingresa tu correo electrónico",
|
||||
"failed": "Inicio de sesión fallido",
|
||||
"forgotPassword": "¿Olvidaste tu 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",
|
||||
@@ -25,12 +26,32 @@
|
||||
"passwordLabel": "Contraseña",
|
||||
"passwordPlaceholder": "Ingresa tu contraseña",
|
||||
"privacyLink": "Política de privacidad",
|
||||
"signInOrSignUp": "Iniciar sesión / Registrarse",
|
||||
"signUp": "Regístrate",
|
||||
"success": "Inicio de sesión exitoso",
|
||||
"termsLink": "Términos de uso",
|
||||
"termsText": "Al hacer clic en \"Siguiente\" o \"Registrarse\", aceptas nuestros",
|
||||
"title": "Inicia sesión en tu cuenta"
|
||||
},
|
||||
"required": {
|
||||
"credits": {
|
||||
"action": "Abrir configuración para añadir créditos",
|
||||
"hint": "Para añadir créditos ve a: Configuración > Usuario > Créditos",
|
||||
"message": "Este flujo de trabajo incluye nodos que requieren créditos. Por favor, añade créditos a tu cuenta para continuar.",
|
||||
"title": "Créditos requeridos para ejecutar el flujo de trabajo"
|
||||
},
|
||||
"signIn": {
|
||||
"action": "Abrir configuración para iniciar sesión",
|
||||
"hint": "Para iniciar sesión ve a: Configuración > Usuario > Iniciar sesión",
|
||||
"message": "Este flujo de trabajo incluye nodos que requieren una cuenta activa. Por favor, inicia sesión o crea una para continuar.",
|
||||
"title": "Inicio de sesión requerido para ejecutar el flujo de trabajo"
|
||||
}
|
||||
},
|
||||
"signOut": {
|
||||
"signOut": "Cerrar sesión",
|
||||
"success": "Sesión cerrada correctamente",
|
||||
"successDetail": "Has cerrado sesión en tu cuenta."
|
||||
},
|
||||
"signup": {
|
||||
"alreadyHaveAccount": "¿Ya tienes una cuenta?",
|
||||
"emailLabel": "Correo electrónico",
|
||||
@@ -96,9 +117,17 @@
|
||||
"credits": "Créditos",
|
||||
"creditsHistory": "Historial de créditos",
|
||||
"faqs": "Preguntas frecuentes",
|
||||
"lastUpdated": "Última actualización",
|
||||
"messageSupport": "Contactar soporte",
|
||||
"paymentDetails": "Detalles de pago",
|
||||
"purchaseCredits": "Comprar créditos",
|
||||
"topUp": {
|
||||
"addCredits": "Agregar créditos a tu saldo",
|
||||
"buyNow": "Comprar ahora",
|
||||
"insufficientMessage": "No tienes suficientes créditos para ejecutar este flujo de trabajo.",
|
||||
"insufficientTitle": "Créditos insuficientes",
|
||||
"maxAmount": "(Máx. $1,000 USD)",
|
||||
"title": "Agregar al saldo de créditos"
|
||||
},
|
||||
"yourCreditBalance": "Tu saldo de créditos"
|
||||
},
|
||||
"dataTypes": {
|
||||
@@ -1096,6 +1125,17 @@
|
||||
"next": "Siguiente",
|
||||
"selectUser": "Selecciona un usuario"
|
||||
},
|
||||
"userSettings": {
|
||||
"email": "Correo electrónico",
|
||||
"name": "Nombre",
|
||||
"notSet": "No establecido",
|
||||
"provider": "Método de inicio de sesión",
|
||||
"providers": {
|
||||
"github": "GitHub",
|
||||
"google": "Google"
|
||||
},
|
||||
"title": "Configuración de usuario"
|
||||
},
|
||||
"validation": {
|
||||
"invalidEmail": "Dirección de correo electrónico inválida",
|
||||
"maxLength": "No debe tener más de {length} caracteres",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"emailPlaceholder": "Entrez votre email",
|
||||
"failed": "Échec de la connexion",
|
||||
"forgotPassword": "Mot de passe oublié?",
|
||||
"genericErrorMessage": "Désolé, une erreur s'est produite. Veuillez contacter {supportEmail}.",
|
||||
"loginButton": "Se connecter",
|
||||
"loginWithGithub": "Se connecter avec Github",
|
||||
"loginWithGoogle": "Se connecter avec Google",
|
||||
@@ -25,12 +26,32 @@
|
||||
"passwordLabel": "Mot de passe",
|
||||
"passwordPlaceholder": "Entrez votre mot de passe",
|
||||
"privacyLink": "Politique de confidentialité",
|
||||
"signInOrSignUp": "Se connecter / S’inscrire",
|
||||
"signUp": "S'inscrire",
|
||||
"success": "Connexion réussie",
|
||||
"termsLink": "Conditions d'utilisation",
|
||||
"termsText": "En cliquant sur \"Suivant\" ou \"S'inscrire\", vous acceptez nos",
|
||||
"title": "Connectez-vous à votre compte"
|
||||
},
|
||||
"required": {
|
||||
"credits": {
|
||||
"action": "Ouvrir les paramètres pour ajouter des crédits",
|
||||
"hint": "Pour ajouter des crédits, allez dans : Paramètres > Utilisateur > Crédits",
|
||||
"message": "Ce workflow inclut des nœuds nécessitant des crédits. Veuillez ajouter des crédits à votre compte pour continuer.",
|
||||
"title": "Crédits requis pour exécuter le workflow"
|
||||
},
|
||||
"signIn": {
|
||||
"action": "Ouvrir les paramètres pour se connecter",
|
||||
"hint": "Pour vous connecter, allez dans : Paramètres > Utilisateur > Connexion",
|
||||
"message": "Ce workflow inclut des nœuds nécessitant un compte actif. Veuillez vous connecter ou en créer un pour continuer.",
|
||||
"title": "Connexion requise pour exécuter le workflow"
|
||||
}
|
||||
},
|
||||
"signOut": {
|
||||
"signOut": "Se déconnecter",
|
||||
"success": "Déconnexion réussie",
|
||||
"successDetail": "Vous avez été déconnecté de votre compte."
|
||||
},
|
||||
"signup": {
|
||||
"alreadyHaveAccount": "Vous avez déjà un compte?",
|
||||
"emailLabel": "Email",
|
||||
@@ -96,9 +117,17 @@
|
||||
"credits": "Crédits",
|
||||
"creditsHistory": "Historique des crédits",
|
||||
"faqs": "FAQ",
|
||||
"lastUpdated": "Dernière mise à jour",
|
||||
"messageSupport": "Contacter le support",
|
||||
"paymentDetails": "Détails de paiement",
|
||||
"purchaseCredits": "Acheter des crédits",
|
||||
"topUp": {
|
||||
"addCredits": "Ajouter des crédits à votre solde",
|
||||
"buyNow": "Acheter maintenant",
|
||||
"insufficientMessage": "Vous n'avez pas assez de crédits pour exécuter ce workflow.",
|
||||
"insufficientTitle": "Crédits insuffisants",
|
||||
"maxAmount": "(Max. 1 000 $ US)",
|
||||
"title": "Ajouter au solde de crédits"
|
||||
},
|
||||
"yourCreditBalance": "Votre solde de crédits"
|
||||
},
|
||||
"dataTypes": {
|
||||
@@ -1096,6 +1125,17 @@
|
||||
"next": "Suivant",
|
||||
"selectUser": "Sélectionnez un utilisateur"
|
||||
},
|
||||
"userSettings": {
|
||||
"email": "E-mail",
|
||||
"name": "Nom",
|
||||
"notSet": "Non défini",
|
||||
"provider": "Méthode de connexion",
|
||||
"providers": {
|
||||
"github": "GitHub",
|
||||
"google": "Google"
|
||||
},
|
||||
"title": "Paramètres utilisateur"
|
||||
},
|
||||
"validation": {
|
||||
"invalidEmail": "Adresse e-mail invalide",
|
||||
"maxLength": "Ne doit pas dépasser {length} caractères",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"emailPlaceholder": "メールアドレスを入力してください",
|
||||
"failed": "ログイン失敗",
|
||||
"forgotPassword": "パスワードを忘れましたか?",
|
||||
"genericErrorMessage": "申し訳ありませんが、エラーが発生しました。{supportEmail} までご連絡ください。",
|
||||
"loginButton": "ログイン",
|
||||
"loginWithGithub": "Githubでログイン",
|
||||
"loginWithGoogle": "Googleでログイン",
|
||||
@@ -25,12 +26,32 @@
|
||||
"passwordLabel": "パスワード",
|
||||
"passwordPlaceholder": "パスワードを入力してください",
|
||||
"privacyLink": "プライバシーポリシー",
|
||||
"signInOrSignUp": "サインイン / サインアップ",
|
||||
"signUp": "サインアップ",
|
||||
"success": "ログイン成功",
|
||||
"termsLink": "利用規約",
|
||||
"termsText": "「次へ」または「サインアップ」をクリックすると、私たちの",
|
||||
"title": "アカウントにログインする"
|
||||
},
|
||||
"required": {
|
||||
"credits": {
|
||||
"action": "設定を開いてクレジットを追加",
|
||||
"hint": "クレジットを追加するには: 設定 > ユーザー > クレジット",
|
||||
"message": "このワークフローにはクレジットが必要なノードが含まれています。続行するにはアカウントにクレジットを追加してください。",
|
||||
"title": "ワークフロー実行にはクレジットが必要です"
|
||||
},
|
||||
"signIn": {
|
||||
"action": "設定を開いてログイン",
|
||||
"hint": "ログインするには: 設定 > ユーザー > ログイン",
|
||||
"message": "このワークフローにはアクティブなアカウントが必要なノードが含まれています。続行するにはログインまたはアカウントを作成してください。",
|
||||
"title": "ワークフロー実行にはサインインが必要です"
|
||||
}
|
||||
},
|
||||
"signOut": {
|
||||
"signOut": "ログアウト",
|
||||
"success": "正常にサインアウトしました",
|
||||
"successDetail": "アカウントからサインアウトしました。"
|
||||
},
|
||||
"signup": {
|
||||
"alreadyHaveAccount": "すでにアカウントをお持ちですか?",
|
||||
"emailLabel": "メール",
|
||||
@@ -96,9 +117,17 @@
|
||||
"credits": "クレジット",
|
||||
"creditsHistory": "クレジット履歴",
|
||||
"faqs": "よくある質問",
|
||||
"lastUpdated": "最終更新",
|
||||
"messageSupport": "サポートにメッセージ",
|
||||
"paymentDetails": "支払い詳細",
|
||||
"purchaseCredits": "クレジットを購入",
|
||||
"topUp": {
|
||||
"addCredits": "残高にクレジットを追加",
|
||||
"buyNow": "今すぐ購入",
|
||||
"insufficientMessage": "このワークフローを実行するのに十分なクレジットがありません。",
|
||||
"insufficientTitle": "クレジット不足",
|
||||
"maxAmount": "(最大 $1,000 USD)",
|
||||
"title": "クレジット残高を追加"
|
||||
},
|
||||
"yourCreditBalance": "あなたのクレジット残高"
|
||||
},
|
||||
"dataTypes": {
|
||||
@@ -1096,6 +1125,17 @@
|
||||
"next": "次へ",
|
||||
"selectUser": "ユーザーを選択"
|
||||
},
|
||||
"userSettings": {
|
||||
"email": "メールアドレス",
|
||||
"name": "名前",
|
||||
"notSet": "未設定",
|
||||
"provider": "サインイン方法",
|
||||
"providers": {
|
||||
"github": "GitHub",
|
||||
"google": "Google"
|
||||
},
|
||||
"title": "ユーザー設定"
|
||||
},
|
||||
"validation": {
|
||||
"invalidEmail": "無効なメールアドレス",
|
||||
"maxLength": "{length}文字以下でなければなりません",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"emailPlaceholder": "이메일을 입력하세요",
|
||||
"failed": "로그인 실패",
|
||||
"forgotPassword": "비밀번호를 잊으셨나요?",
|
||||
"genericErrorMessage": "죄송합니다. 오류가 발생했습니다. {supportEmail}로 문의해 주세요.",
|
||||
"loginButton": "로그인",
|
||||
"loginWithGithub": "Github로 로그인",
|
||||
"loginWithGoogle": "구글로 로그인",
|
||||
@@ -25,12 +26,32 @@
|
||||
"passwordLabel": "비밀번호",
|
||||
"passwordPlaceholder": "비밀번호를 입력하세요",
|
||||
"privacyLink": "개인정보 보호정책",
|
||||
"signInOrSignUp": "로그인 / 회원가입",
|
||||
"signUp": "가입하기",
|
||||
"success": "로그인 성공",
|
||||
"termsLink": "이용 약관",
|
||||
"termsText": "\"다음\" 또는 \"가입하기\"를 클릭하면 우리의",
|
||||
"title": "계정에 로그인"
|
||||
},
|
||||
"required": {
|
||||
"credits": {
|
||||
"action": "설정에서 크레딧 추가 열기",
|
||||
"hint": "크레딧을 추가하려면: 설정 > 사용자 > 크레딧 으로 이동하세요.",
|
||||
"message": "이 워크플로에는 크레딧이 필요한 노드가 포함되어 있습니다. 계속하려면 계정에 크레딧을 추가하세요.",
|
||||
"title": "워크플로 실행을 위한 크레딧 필요"
|
||||
},
|
||||
"signIn": {
|
||||
"action": "설정에서 로그인 열기",
|
||||
"hint": "로그인하려면: 설정 > 사용자 > 로그인 으로 이동하세요.",
|
||||
"message": "이 워크플로에는 활성 계정이 필요한 노드가 포함되어 있습니다. 계속하려면 로그인하거나 계정을 생성하세요.",
|
||||
"title": "워크플로 실행을 위한 로그인 필요"
|
||||
}
|
||||
},
|
||||
"signOut": {
|
||||
"signOut": "로그아웃",
|
||||
"success": "성공적으로 로그아웃되었습니다",
|
||||
"successDetail": "계정에서 로그아웃되었습니다."
|
||||
},
|
||||
"signup": {
|
||||
"alreadyHaveAccount": "이미 계정이 있으신가요?",
|
||||
"emailLabel": "이메일",
|
||||
@@ -96,9 +117,17 @@
|
||||
"credits": "크레딧",
|
||||
"creditsHistory": "크레딧 내역",
|
||||
"faqs": "자주 묻는 질문",
|
||||
"lastUpdated": "마지막 업데이트",
|
||||
"messageSupport": "지원 문의",
|
||||
"paymentDetails": "결제 정보",
|
||||
"purchaseCredits": "크레딧 구매",
|
||||
"topUp": {
|
||||
"addCredits": "잔액에 크레딧 추가",
|
||||
"buyNow": "지금 구매",
|
||||
"insufficientMessage": "이 워크플로우를 실행하기에 크레딧이 부족합니다.",
|
||||
"insufficientTitle": "크레딧 부족",
|
||||
"maxAmount": "(최대 $1,000 USD)",
|
||||
"title": "크레딧 잔액 충전"
|
||||
},
|
||||
"yourCreditBalance": "보유 크레딧 잔액"
|
||||
},
|
||||
"dataTypes": {
|
||||
@@ -1096,6 +1125,17 @@
|
||||
"next": "다음",
|
||||
"selectUser": "사용자 선택"
|
||||
},
|
||||
"userSettings": {
|
||||
"email": "이메일",
|
||||
"name": "이름",
|
||||
"notSet": "설정되지 않음",
|
||||
"provider": "로그인 방법",
|
||||
"providers": {
|
||||
"github": "GitHub",
|
||||
"google": "Google"
|
||||
},
|
||||
"title": "사용자 설정"
|
||||
},
|
||||
"validation": {
|
||||
"invalidEmail": "유효하지 않은 이메일 주소",
|
||||
"maxLength": "{length}자를 초과할 수 없습니다",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"emailPlaceholder": "Введите вашу электронную почту",
|
||||
"failed": "Вход не удался",
|
||||
"forgotPassword": "Забыли пароль?",
|
||||
"genericErrorMessage": "Извините, произошла ошибка. Пожалуйста, свяжитесь с {supportEmail}.",
|
||||
"loginButton": "Войти",
|
||||
"loginWithGithub": "Войти через Github",
|
||||
"loginWithGoogle": "Войти через Google",
|
||||
@@ -25,12 +26,32 @@
|
||||
"passwordLabel": "Пароль",
|
||||
"passwordPlaceholder": "Введите ваш пароль",
|
||||
"privacyLink": "Политикой конфиденциальности",
|
||||
"signInOrSignUp": "Войти / Зарегистрироваться",
|
||||
"signUp": "Зарегистрироваться",
|
||||
"success": "Вход выполнен успешно",
|
||||
"termsLink": "Условиями использования",
|
||||
"termsText": "Нажимая \"Далее\" или \"Зарегистрироваться\", вы соглашаетесь с нашими",
|
||||
"title": "Войдите в свой аккаунт"
|
||||
},
|
||||
"required": {
|
||||
"credits": {
|
||||
"action": "Открыть настройки для пополнения кредитов",
|
||||
"hint": "Чтобы пополнить кредиты, перейдите в: Настройки > Пользователь > Кредиты",
|
||||
"message": "Этот рабочий процесс содержит узлы, для которых необходимы кредиты. Пожалуйста, пополните баланс, чтобы продолжить.",
|
||||
"title": "Требуются кредиты для выполнения рабочего процесса"
|
||||
},
|
||||
"signIn": {
|
||||
"action": "Открыть настройки для входа",
|
||||
"hint": "Чтобы войти, перейдите в: Настройки > Пользователь > Вход",
|
||||
"message": "Этот рабочий процесс содержит узлы, для которых необходима активная учетная запись. Пожалуйста, войдите или создайте учетную запись, чтобы продолжить.",
|
||||
"title": "Требуется вход для выполнения рабочего процесса"
|
||||
}
|
||||
},
|
||||
"signOut": {
|
||||
"signOut": "Выйти",
|
||||
"success": "Вы успешно вышли из системы",
|
||||
"successDetail": "Вы вышли из своей учетной записи."
|
||||
},
|
||||
"signup": {
|
||||
"alreadyHaveAccount": "Уже есть аккаунт?",
|
||||
"emailLabel": "Электронная почта",
|
||||
@@ -96,9 +117,17 @@
|
||||
"credits": "Кредиты",
|
||||
"creditsHistory": "История кредитов",
|
||||
"faqs": "Часто задаваемые вопросы",
|
||||
"lastUpdated": "Последнее обновление",
|
||||
"messageSupport": "Связаться с поддержкой",
|
||||
"paymentDetails": "Детали оплаты",
|
||||
"purchaseCredits": "Купить кредиты",
|
||||
"topUp": {
|
||||
"addCredits": "Добавить кредиты на баланс",
|
||||
"buyNow": "Купить сейчас",
|
||||
"insufficientMessage": "У вас недостаточно кредитов для запуска этого рабочего процесса.",
|
||||
"insufficientTitle": "Недостаточно кредитов",
|
||||
"maxAmount": "(Макс. $1,000 USD)",
|
||||
"title": "Пополнить баланс кредитов"
|
||||
},
|
||||
"yourCreditBalance": "Ваш баланс кредитов"
|
||||
},
|
||||
"dataTypes": {
|
||||
@@ -1096,6 +1125,17 @@
|
||||
"next": "Далее",
|
||||
"selectUser": "Выберите пользователя"
|
||||
},
|
||||
"userSettings": {
|
||||
"email": "Электронная почта",
|
||||
"name": "Имя",
|
||||
"notSet": "Не задано",
|
||||
"provider": "Способ входа",
|
||||
"providers": {
|
||||
"github": "GitHub",
|
||||
"google": "Google"
|
||||
},
|
||||
"title": "Настройки пользователя"
|
||||
},
|
||||
"validation": {
|
||||
"invalidEmail": "Недействительный адрес электронной почты",
|
||||
"maxLength": "Должно быть не более {length} символов",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"emailPlaceholder": "输入您的电子邮件",
|
||||
"failed": "登录失败",
|
||||
"forgotPassword": "忘记密码?",
|
||||
"genericErrorMessage": "抱歉,我们遇到了一些错误。请联系 {supportEmail}。",
|
||||
"loginButton": "登录",
|
||||
"loginWithGithub": "使用Github登录",
|
||||
"loginWithGoogle": "使用Google登录",
|
||||
@@ -25,12 +26,32 @@
|
||||
"passwordLabel": "密码",
|
||||
"passwordPlaceholder": "输入您的密码",
|
||||
"privacyLink": "隐私政策",
|
||||
"signInOrSignUp": "登录 / 注册",
|
||||
"signUp": "注册",
|
||||
"success": "登录成功",
|
||||
"termsLink": "使用条款",
|
||||
"termsText": "点击“下一步”或“注册”即表示您同意我们的",
|
||||
"title": "登录您的账户"
|
||||
},
|
||||
"required": {
|
||||
"credits": {
|
||||
"action": "打开设置进行充值",
|
||||
"hint": "要充值,请前往:设置 > 用户 > 积分",
|
||||
"message": "此工作流包含需要积分的节点。请为您的账户充值以继续。",
|
||||
"title": "需要积分以执行工作流"
|
||||
},
|
||||
"signIn": {
|
||||
"action": "打开设置进行登录",
|
||||
"hint": "要登录,请前往:设置 > 用户 > 登录",
|
||||
"message": "此工作流包含需要活跃账户的节点。请登录或创建账户以继续。",
|
||||
"title": "需要登录以执行工作流"
|
||||
}
|
||||
},
|
||||
"signOut": {
|
||||
"signOut": "退出登录",
|
||||
"success": "成功退出登录",
|
||||
"successDetail": "您已成功退出账户。"
|
||||
},
|
||||
"signup": {
|
||||
"alreadyHaveAccount": "已经有账户了?",
|
||||
"emailLabel": "电子邮件",
|
||||
@@ -96,9 +117,17 @@
|
||||
"credits": "积分",
|
||||
"creditsHistory": "积分历史",
|
||||
"faqs": "常见问题",
|
||||
"lastUpdated": "最近更新",
|
||||
"messageSupport": "联系客服",
|
||||
"paymentDetails": "支付详情",
|
||||
"purchaseCredits": "购买积分",
|
||||
"topUp": {
|
||||
"addCredits": "为您的余额充值",
|
||||
"buyNow": "立即购买",
|
||||
"insufficientMessage": "您的积分不足,无法运行此工作流。",
|
||||
"insufficientTitle": "积分不足",
|
||||
"maxAmount": "(最高 $1,000 美元)",
|
||||
"title": "充值余额"
|
||||
},
|
||||
"yourCreditBalance": "您的积分余额"
|
||||
},
|
||||
"dataTypes": {
|
||||
@@ -1096,6 +1125,17 @@
|
||||
"next": "下一步",
|
||||
"selectUser": "选择用户"
|
||||
},
|
||||
"userSettings": {
|
||||
"email": "电子邮件",
|
||||
"name": "名称",
|
||||
"notSet": "未设置",
|
||||
"provider": "登录方式",
|
||||
"providers": {
|
||||
"github": "GitHub",
|
||||
"google": "Google"
|
||||
},
|
||||
"title": "用户设置"
|
||||
},
|
||||
"validation": {
|
||||
"invalidEmail": "无效的电子邮件地址",
|
||||
"maxLength": "不能超过{length}个字符",
|
||||
|
||||
@@ -673,7 +673,17 @@ export class ComfyApp {
|
||||
})
|
||||
|
||||
api.addEventListener('execution_error', ({ detail }) => {
|
||||
useDialogService().showExecutionErrorDialog(detail)
|
||||
// Check if this is an auth-related error or credits-related error
|
||||
if (detail.exception_message === 'Please login first to use this node.') {
|
||||
useDialogService().showSignInRequiredDialog({ type: 'signIn' })
|
||||
} else if (
|
||||
detail.exception_message ===
|
||||
'Payment Required: Please add credits to your account to use this node.'
|
||||
) {
|
||||
useDialogService().showSignInRequiredDialog({ type: 'credits' })
|
||||
} else {
|
||||
useDialogService().showExecutionErrorDialog(detail)
|
||||
}
|
||||
this.canvas.draw(true, true)
|
||||
})
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ import MissingModelsWarning from '@/components/dialog/content/MissingModelsWarni
|
||||
import PromptDialogContent from '@/components/dialog/content/PromptDialogContent.vue'
|
||||
import SettingDialogContent from '@/components/dialog/content/SettingDialogContent.vue'
|
||||
import SignInContent from '@/components/dialog/content/SignInContent.vue'
|
||||
import SignInRequiredDialogContent from '@/components/dialog/content/SignInRequiredDialogContent.vue'
|
||||
import TopUpCreditsDialogContent from '@/components/dialog/content/TopUpCreditsDialogContent.vue'
|
||||
import ManagerDialogContent from '@/components/dialog/content/manager/ManagerDialogContent.vue'
|
||||
import ManagerHeader from '@/components/dialog/content/manager/ManagerHeader.vue'
|
||||
import ManagerProgressFooter from '@/components/dialog/footer/ManagerProgressFooter.vue'
|
||||
@@ -51,7 +53,13 @@ export const useDialogService = () => {
|
||||
}
|
||||
|
||||
function showSettingsDialog(
|
||||
panel?: 'about' | 'keybinding' | 'extension' | 'server-config'
|
||||
panel?:
|
||||
| 'about'
|
||||
| 'keybinding'
|
||||
| 'extension'
|
||||
| 'server-config'
|
||||
| 'user'
|
||||
| 'credits'
|
||||
) {
|
||||
const props = panel ? { props: { defaultPanel: panel } } : undefined
|
||||
|
||||
@@ -340,6 +348,29 @@ export const useDialogService = () => {
|
||||
})
|
||||
}
|
||||
|
||||
function showSignInRequiredDialog(options: { type: 'signIn' | 'credits' }) {
|
||||
dialogStore.showDialog({
|
||||
key: 'signin-required',
|
||||
component: SignInRequiredDialogContent,
|
||||
props: options
|
||||
})
|
||||
}
|
||||
|
||||
function showTopUpCreditsDialog(options?: {
|
||||
isInsufficientCredits?: boolean
|
||||
}) {
|
||||
return dialogStore.showDialog({
|
||||
key: 'top-up-credits',
|
||||
component: TopUpCreditsDialogContent,
|
||||
props: options,
|
||||
dialogComponentProps: {
|
||||
pt: {
|
||||
header: { class: '!p-3' }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
showLoadWorkflowWarning,
|
||||
showMissingModelsWarning,
|
||||
@@ -353,6 +384,8 @@ export const useDialogService = () => {
|
||||
showErrorDialog,
|
||||
showApiNodesSignInDialog,
|
||||
showSignInDialog,
|
||||
showSignInRequiredDialog,
|
||||
showTopUpCreditsDialog,
|
||||
prompt,
|
||||
confirm
|
||||
}
|
||||
|
||||
@@ -16,12 +16,24 @@ import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useFirebaseAuth } from 'vuefire'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { operations } from '@/types/comfyRegistryTypes'
|
||||
|
||||
import { useToastStore } from './toastStore'
|
||||
|
||||
type CreditPurchaseResponse =
|
||||
operations['InitiateCreditPurchase']['responses']['201']['content']['application/json']
|
||||
type CreditPurchasePayload =
|
||||
operations['InitiateCreditPurchase']['requestBody']['content']['application/json']
|
||||
type CreateCustomerResponse =
|
||||
operations['createCustomer']['responses']['201']['content']['application/json']
|
||||
type GetCustomerBalanceResponse =
|
||||
operations['GetCustomerBalance']['responses']['200']['content']['application/json']
|
||||
type AccessBillingPortalResponse =
|
||||
operations['AccessBillingPortal']['responses']['200']['content']['application/json']
|
||||
type AccessBillingPortalReqBody =
|
||||
operations['AccessBillingPortal']['requestBody']
|
||||
|
||||
// TODO: Switch to prod api based on environment (requires prod api to be ready)
|
||||
const API_BASE_URL = 'https://stagingapi.comfy.org'
|
||||
@@ -32,6 +44,11 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
const error = ref<string | null>(null)
|
||||
const currentUser = ref<User | null>(null)
|
||||
const isInitialized = ref(false)
|
||||
const customerCreated = ref(false)
|
||||
|
||||
// Balance state
|
||||
const balance = ref<GetCustomerBalanceResponse | null>(null)
|
||||
const lastBalanceUpdateTime = ref<Date | null>(null)
|
||||
|
||||
// Providers
|
||||
const googleProvider = new GoogleAuthProvider()
|
||||
@@ -51,13 +68,92 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
onAuthStateChanged(auth, (user) => {
|
||||
currentUser.value = user
|
||||
isInitialized.value = true
|
||||
|
||||
// Reset balance when auth state changes
|
||||
balance.value = null
|
||||
lastBalanceUpdateTime.value = null
|
||||
})
|
||||
} else {
|
||||
error.value = 'Firebase Auth not available from VueFire'
|
||||
}
|
||||
|
||||
const showAuthErrorToast = () => {
|
||||
useToastStore().add({
|
||||
summary: t('g.error'),
|
||||
detail: t('auth.login.genericErrorMessage', {
|
||||
supportEmail: 'support@comfy.org'
|
||||
}),
|
||||
severity: 'error'
|
||||
})
|
||||
}
|
||||
|
||||
const getIdToken = async (): Promise<string | null> => {
|
||||
if (currentUser.value) {
|
||||
return currentUser.value.getIdToken()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const fetchBalance = async (): Promise<GetCustomerBalanceResponse | null> => {
|
||||
const token = await getIdToken()
|
||||
if (!token) {
|
||||
error.value = 'Cannot fetch balance: User not authenticated'
|
||||
return null
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/customers/balance`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
// Customer not found is expected for new users
|
||||
return null
|
||||
}
|
||||
const errorData = await response.json()
|
||||
error.value = `Failed to fetch balance: ${errorData.message}`
|
||||
return null
|
||||
}
|
||||
|
||||
const balanceData = await response.json()
|
||||
// Update the last balance update time
|
||||
lastBalanceUpdateTime.value = new Date()
|
||||
balance.value = balanceData
|
||||
return balanceData
|
||||
}
|
||||
|
||||
const createCustomer = async (
|
||||
token: string
|
||||
): Promise<CreateCustomerResponse> => {
|
||||
const createCustomerRes = await fetch(`${API_BASE_URL}/customers`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
if (!createCustomerRes.ok) {
|
||||
throw new Error(
|
||||
`Failed to create customer: ${createCustomerRes.statusText}`
|
||||
)
|
||||
}
|
||||
|
||||
const createCustomerResJson: CreateCustomerResponse =
|
||||
await createCustomerRes.json()
|
||||
if (!createCustomerResJson?.id) {
|
||||
throw new Error('Failed to create customer: No customer ID returned')
|
||||
}
|
||||
|
||||
return createCustomerResJson
|
||||
}
|
||||
|
||||
const executeAuthAction = async <T>(
|
||||
action: (auth: Auth) => Promise<T>
|
||||
action: (auth: Auth) => Promise<T>,
|
||||
options: {
|
||||
createCustomer?: boolean
|
||||
} = {}
|
||||
): Promise<T> => {
|
||||
if (!auth) throw new Error('Firebase Auth not initialized')
|
||||
|
||||
@@ -65,9 +161,21 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
return await action(auth)
|
||||
const result = await action(auth)
|
||||
|
||||
// Create customer if needed
|
||||
if (options?.createCustomer) {
|
||||
const token = await getIdToken()
|
||||
if (!token) {
|
||||
throw new Error('Cannot create customer: User not authenticated')
|
||||
}
|
||||
await createCustomer(token)
|
||||
}
|
||||
|
||||
return result
|
||||
} catch (e: unknown) {
|
||||
error.value = e instanceof Error ? e.message : 'Unknown error'
|
||||
showAuthErrorToast()
|
||||
throw e
|
||||
} finally {
|
||||
loading.value = false
|
||||
@@ -78,38 +186,38 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
email: string,
|
||||
password: string
|
||||
): Promise<UserCredential> =>
|
||||
executeAuthAction((authInstance) =>
|
||||
signInWithEmailAndPassword(authInstance, email, password)
|
||||
executeAuthAction(
|
||||
(authInstance) =>
|
||||
signInWithEmailAndPassword(authInstance, email, password),
|
||||
{ createCustomer: true }
|
||||
)
|
||||
|
||||
const register = async (
|
||||
email: string,
|
||||
password: string
|
||||
): Promise<UserCredential> =>
|
||||
executeAuthAction((authInstance) =>
|
||||
createUserWithEmailAndPassword(authInstance, email, password)
|
||||
): Promise<UserCredential> => {
|
||||
return executeAuthAction(
|
||||
(authInstance) =>
|
||||
createUserWithEmailAndPassword(authInstance, email, password),
|
||||
{ createCustomer: true }
|
||||
)
|
||||
}
|
||||
|
||||
const loginWithGoogle = async (): Promise<UserCredential> =>
|
||||
executeAuthAction((authInstance) =>
|
||||
signInWithPopup(authInstance, googleProvider)
|
||||
executeAuthAction(
|
||||
(authInstance) => signInWithPopup(authInstance, googleProvider),
|
||||
{ createCustomer: true }
|
||||
)
|
||||
|
||||
const loginWithGithub = async (): Promise<UserCredential> =>
|
||||
executeAuthAction((authInstance) =>
|
||||
signInWithPopup(authInstance, githubProvider)
|
||||
executeAuthAction(
|
||||
(authInstance) => signInWithPopup(authInstance, githubProvider),
|
||||
{ createCustomer: true }
|
||||
)
|
||||
|
||||
const logout = async (): Promise<void> =>
|
||||
executeAuthAction((authInstance) => signOut(authInstance))
|
||||
|
||||
const getIdToken = async (): Promise<string | null> => {
|
||||
if (currentUser.value) {
|
||||
return currentUser.value.getIdToken()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const addCredits = async (
|
||||
requestBodyContent: CreditPurchasePayload
|
||||
): Promise<CreditPurchaseResponse | null> => {
|
||||
@@ -119,6 +227,12 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
return null
|
||||
}
|
||||
|
||||
// Ensure customer was created during login/registration
|
||||
if (!customerCreated.value) {
|
||||
await createCustomer(token)
|
||||
customerCreated.value = true
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/customers/credit`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -134,7 +248,6 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
return null
|
||||
}
|
||||
|
||||
// TODO: start polling /listBalance until balance is updated or n retries fail or report no change
|
||||
return response.json()
|
||||
}
|
||||
|
||||
@@ -143,12 +256,51 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
): Promise<CreditPurchaseResponse | null> =>
|
||||
executeAuthAction((_) => addCredits(requestBodyContent))
|
||||
|
||||
const openSignInPanel = () => {
|
||||
useDialogService().showSettingsDialog('user')
|
||||
}
|
||||
|
||||
const openCreditsPanel = () => {
|
||||
useDialogService().showSettingsDialog('credits')
|
||||
}
|
||||
|
||||
const accessBillingPortal = async (
|
||||
requestBody?: AccessBillingPortalReqBody
|
||||
): Promise<AccessBillingPortalResponse | null> => {
|
||||
const token = await getIdToken()
|
||||
if (!token) {
|
||||
error.value = 'Cannot access billing portal: User not authenticated'
|
||||
return null
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/customers/billing`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
...(requestBody && {
|
||||
body: JSON.stringify(requestBody)
|
||||
})
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
error.value = `Failed to access billing portal: ${errorData.message}`
|
||||
return null
|
||||
}
|
||||
|
||||
return response.json()
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
loading,
|
||||
error,
|
||||
currentUser,
|
||||
isInitialized,
|
||||
balance,
|
||||
lastBalanceUpdateTime,
|
||||
|
||||
// Getters
|
||||
isAuthenticated,
|
||||
@@ -162,6 +314,10 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
getIdToken,
|
||||
loginWithGoogle,
|
||||
loginWithGithub,
|
||||
initiateCreditPurchase
|
||||
initiateCreditPurchase,
|
||||
openSignInPanel,
|
||||
openCreditsPanel,
|
||||
fetchBalance,
|
||||
accessBillingPortal
|
||||
}
|
||||
})
|
||||
|
||||
@@ -416,6 +416,49 @@ export function compareVersions(
|
||||
return 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a currency amount to Metronome's integer representation.
|
||||
* For USD, converts to cents (multiplied by 100).
|
||||
* For all other currencies (including custom pricing units), returns the amount as is.
|
||||
* This is specific to Metronome's API requirements.
|
||||
*
|
||||
* @param amount - The amount in currency to convert
|
||||
* @param currency - The currency to convert
|
||||
* @returns The amount in Metronome's integer format (cents for USD, base units for others)
|
||||
* @example
|
||||
* toMetronomeCurrency(1.23, 'usd') // returns 123 (cents)
|
||||
* toMetronomeCurrency(1000, 'jpy') // returns 1000 (yen)
|
||||
*/
|
||||
export function toMetronomeCurrency(amount: number, currency: string): number {
|
||||
if (currency === 'usd') {
|
||||
return Math.round(amount * 100)
|
||||
}
|
||||
return amount
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts Metronome's integer amount back to a formatted currency string.
|
||||
* For USD, converts from cents to dollars.
|
||||
* For all other currencies (including custom pricing units), returns the amount as is.
|
||||
* This is specific to Metronome's API requirements.
|
||||
*
|
||||
* @param amount - The amount in Metronome's integer format (cents for USD, base units for others)
|
||||
* @param currency - The currency to convert
|
||||
* @returns The formatted amount in currency with 2 decimal places for USD
|
||||
* @example
|
||||
* formatMetronomeCurrency(123, 'usd') // returns "1.23" (cents to USD)
|
||||
* formatMetronomeCurrency(1000, 'jpy') // returns "1000" (yen)
|
||||
*/
|
||||
export function formatMetronomeCurrency(
|
||||
amount: number,
|
||||
currency: string
|
||||
): string {
|
||||
if (currency === 'usd') {
|
||||
return (amount / 100).toFixed(2)
|
||||
}
|
||||
return amount.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a USD amount to microdollars (1/1,000,000 of a dollar).
|
||||
* This conversion is commonly used in financial systems to avoid floating-point precision issues
|
||||
|
||||
Reference in New Issue
Block a user