mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-03 14:54:37 +00:00
[Backport] update subscription panel for new designs (#6397)
## Summary Backport of PR #6378 to `rh-test` branch. ## Changes - Extract credit calculations into useSubscriptionCredits composable - Extract action handlers into useSubscriptionActions composable - Add comprehensive component and unit tests - Update subscription panel layout to match Figma design exactly - Add proper design tokens for modal card surfaces - Update terminology from "API Nodes" to "Partner Nodes" - Make credit breakdown dynamic with real API data - Add proper loading states and error handling - Remove unused tailwindcss eslint dependencies ## Conflicts Resolved - Resolved merge conflicts in `packages/design-system/src/css/style.css` related to button surface CSS variables ## Test plan - Existing tests pass - New tests for subscription composables and components ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6397-Backport-update-subscription-panel-for-new-designs-29c6d73d3650812aaa12ff242fd5e078) by [Unito](https://www.unito.io) --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: GitHub Action <action@github.com> Co-authored-by: Johnpaul Chiwetelu <49923152+Myestery@users.noreply.github.com>
This commit is contained in:
@@ -126,6 +126,11 @@
|
||||
--content-hover-bg: #adadad;
|
||||
--content-hover-fg: #000;
|
||||
|
||||
--button-surface: var(--color-white);
|
||||
--button-surface-contrast: var(--color-black);
|
||||
|
||||
--modal-card-button-surface: var(--color-smoke-300);
|
||||
|
||||
/* Code styling colors for help menu*/
|
||||
--code-text-color: rgb(0 122 255 / 1);
|
||||
--code-bg-color: rgb(96 165 250 / 0.2);
|
||||
@@ -168,6 +173,15 @@
|
||||
.dark-theme {
|
||||
--accent-primary: var(--color-pure-white);
|
||||
--backdrop: var(--color-neutral-900);
|
||||
|
||||
--button-surface: var(--color-charcoal-600);
|
||||
--button-surface-contrast: var(--color-white);
|
||||
--button-hover-surface: var(--color-charcoal-600);
|
||||
--button-active-surface: var(--color-charcoal-600);
|
||||
--button-icon: var(--color-smoke-800);
|
||||
|
||||
--modal-card-button-surface: var(--color-charcoal-300);
|
||||
|
||||
--dialog-surface: var(--color-neutral-700);
|
||||
--node-component-border: var(--color-stone-200);
|
||||
--node-component-border-error: var(--color-danger-100);
|
||||
@@ -196,6 +210,12 @@
|
||||
|
||||
@theme inline {
|
||||
--color-backdrop: var(--backdrop);
|
||||
--color-button-active-surface: var(--button-active-surface);
|
||||
--color-button-hover-surface: var(--button-hover-surface);
|
||||
--color-button-icon: var(--button-icon);
|
||||
--color-button-surface: var(--button-surface);
|
||||
--color-button-surface-contrast: var(--button-surface-contrast);
|
||||
--color-modal-card-button-surface: var(--modal-card-button-surface);
|
||||
--color-dialog-surface: var(--dialog-surface);
|
||||
--color-node-component-border: var(--node-component-border);
|
||||
--color-node-component-executing: var(--node-component-executing);
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
>
|
||||
{{ badge.label }}
|
||||
</div>
|
||||
<div class="text-sm font-semibold">{{ badge.text }}</div>
|
||||
<div class="text-sm font-inter">{{ badge.text }}</div>
|
||||
<div v-if="badge.tooltip" class="text-xs">
|
||||
{{ badge.tooltip }}
|
||||
</div>
|
||||
@@ -90,7 +90,7 @@
|
||||
>
|
||||
{{ badge.label }}
|
||||
</div>
|
||||
<div class="text-sm font-semibold">{{ badge.text }}</div>
|
||||
<div class="text-sm font-inter">{{ badge.text }}</div>
|
||||
<div v-if="badge.tooltip" class="text-xs">
|
||||
{{ badge.tooltip }}
|
||||
</div>
|
||||
@@ -117,7 +117,7 @@
|
||||
>
|
||||
{{ badge.label }}
|
||||
</div>
|
||||
<div class="font-inter text-sm font-extrabold" :class="textClasses">
|
||||
<div class="font-inter text-sm" :class="textClasses">
|
||||
{{ badge.text }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1931,18 +1931,24 @@
|
||||
"renewsDate": "Renews {date}",
|
||||
"expiresDate": "Expires {date}",
|
||||
"manageSubscription": "Manage subscription",
|
||||
"apiNodesBalance": "\"API Nodes\" Credit Balance",
|
||||
"apiNodesDescription": "For running commercial/proprietary models",
|
||||
"partnerNodesBalance": "\"Partner Nodes\" Credit Balance",
|
||||
"partnerNodesDescription": "For running commercial/proprietary models",
|
||||
"totalCredits": "Total credits",
|
||||
"viewUsageHistory": "View usage history",
|
||||
"addApiCredits": "Add API credits",
|
||||
"addCredits": "Add credits",
|
||||
"monthlyCreditsRollover": "These credits will rollover to the next month",
|
||||
"monthlyBonusDescription": "Monthly credit bonus",
|
||||
"prepaidDescription": "Pre-paid credits",
|
||||
"prepaidCreditsInfo": "Credits purchased separately that don't expire",
|
||||
"nextBillingCycle": "next billing cycle",
|
||||
"yourPlanIncludes": "Your plan includes:",
|
||||
"viewMoreDetails": "View more details",
|
||||
"learnMore": "Learn more",
|
||||
"messageSupport": "Message support",
|
||||
"invoiceHistory": "Invoice history",
|
||||
"benefits": {
|
||||
"benefit1": "$10 in monthly credits for API models — top up when needed",
|
||||
"benefit1": "Monthly credits for Partner Nodes — top up when needed",
|
||||
"benefit2": "Up to 30 min runtime per job"
|
||||
},
|
||||
"required": {
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col items-start gap-1 self-stretch">
|
||||
<div class="flex items-start gap-2">
|
||||
<i class="pi pi-check mt-1 text-sm" />
|
||||
<span class="text-sm">
|
||||
<i class="pi pi-check mt-1 text-xs text-text-primary" />
|
||||
<span class="text-sm text-text-primary">
|
||||
{{ $t('subscription.benefits.benefit1') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start gap-2 pb-2">
|
||||
<i class="pi pi-check mt-1 text-sm" />
|
||||
<span class="text-sm">
|
||||
<div class="flex items-start gap-2">
|
||||
<i class="pi pi-check mt-1 text-xs text-text-primary" />
|
||||
<span class="text-sm text-text-primary">
|
||||
{{ $t('subscription.benefits.benefit2') }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -19,8 +19,15 @@
|
||||
text
|
||||
icon="pi pi-external-link"
|
||||
icon-pos="left"
|
||||
size="small"
|
||||
class="self-start !p-0 text-sm hover:!bg-transparent [&]:!text-[inherit]"
|
||||
class="flex h-8 min-h-6 py-2 px-0 items-center gap-2 rounded text-text-secondary"
|
||||
:pt="{
|
||||
icon: {
|
||||
class: 'text-xs text-text-secondary'
|
||||
},
|
||||
label: {
|
||||
class: 'text-sm text-text-secondary'
|
||||
}
|
||||
}"
|
||||
@click="handleViewMoreDetails"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<TabPanel value="PlanCredits" class="subscription-container h-full">
|
||||
<div class="flex h-full flex-col">
|
||||
<div class="flex items-center gap-2">
|
||||
<h2 class="text-2xl">
|
||||
<div class="flex h-full flex-col gap-6">
|
||||
<div class="flex items-baseline gap-2">
|
||||
<span class="text-2xl font-inter font-semibold leading-tight">
|
||||
{{ $t('subscription.title') }}
|
||||
</h2>
|
||||
</span>
|
||||
<CloudBadge
|
||||
reverse-order
|
||||
background-color="var(--p-dialog-background)"
|
||||
@@ -12,17 +12,20 @@
|
||||
</div>
|
||||
|
||||
<div class="grow overflow-auto">
|
||||
<div class="rounded-lg border border-charcoal-400 p-4">
|
||||
<div class="rounded-2xl border border-interface-stroke p-6">
|
||||
<div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<div class="flex items-baseline gap-1">
|
||||
<span class="text-2xl font-bold">{{
|
||||
formattedMonthlyPrice
|
||||
<div class="flex items-baseline gap-1 font-inter font-semibold">
|
||||
<span class="text-2xl">{{ formattedMonthlyPrice }}</span>
|
||||
<span class="text-base">{{
|
||||
$t('subscription.perMonth')
|
||||
}}</span>
|
||||
<span>{{ $t('subscription.perMonth') }}</span>
|
||||
</div>
|
||||
<div v-if="isActiveSubscription" class="text-xs text-muted">
|
||||
<div
|
||||
v-if="isActiveSubscription"
|
||||
class="text-sm text-text-secondary"
|
||||
>
|
||||
<template v-if="isCancelled">
|
||||
{{
|
||||
$t('subscription.expiresDate', {
|
||||
@@ -43,7 +46,15 @@
|
||||
v-if="isActiveSubscription"
|
||||
:label="$t('subscription.manageSubscription')"
|
||||
severity="secondary"
|
||||
class="text-xs"
|
||||
class="text-xs bg-interface-menu-component-surface-selected"
|
||||
:pt="{
|
||||
root: {
|
||||
style: 'border-radius: 8px; padding: 8px 16px;'
|
||||
},
|
||||
label: {
|
||||
class: 'text-text-primary'
|
||||
}
|
||||
}"
|
||||
@click="manageSubscription"
|
||||
/>
|
||||
<SubscribeButton
|
||||
@@ -56,92 +67,143 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-6 rounded-lg pt-10 lg:grid-cols-2">
|
||||
<div class="flex flex-col">
|
||||
<div class="grid grid-cols-1 gap-6 pt-9 lg:grid-cols-2">
|
||||
<div class="flex flex-col flex-1">
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex flex-col">
|
||||
<div class="text-sm">
|
||||
{{ $t('subscription.apiNodesBalance') }}
|
||||
{{ $t('subscription.partnerNodesBalance') }}
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="text-xs text-muted">
|
||||
{{ $t('subscription.apiNodesDescription') }}
|
||||
<div class="text-sm text-muted">
|
||||
{{ $t('subscription.partnerNodesDescription') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex flex-col gap-3 rounded-lg border p-4 dark-theme:border-0 dark-theme:bg-charcoal-600"
|
||||
:class="
|
||||
cn(
|
||||
'relative flex flex-col gap-6 rounded-2xl p-5',
|
||||
'bg-smoke-100 dark-theme:bg-charcoal-600'
|
||||
)
|
||||
"
|
||||
>
|
||||
<Button
|
||||
v-tooltip="refreshTooltip"
|
||||
icon="pi pi-sync"
|
||||
text
|
||||
size="small"
|
||||
class="absolute top-0.5 right-0"
|
||||
:loading="isLoadingBalance"
|
||||
:pt="{
|
||||
icon: {
|
||||
class: 'text-text-secondary text-xs'
|
||||
},
|
||||
loadingIcon: {
|
||||
class: 'text-text-secondary text-xs'
|
||||
}
|
||||
}"
|
||||
@click="handleRefresh"
|
||||
/>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="text-sm text-text-secondary">
|
||||
{{ $t('subscription.totalCredits') }}
|
||||
</div>
|
||||
<Skeleton
|
||||
v-if="isLoadingBalance"
|
||||
width="8rem"
|
||||
height="2rem"
|
||||
/>
|
||||
<div v-else class="text-2xl font-bold">
|
||||
${{ totalCredits }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Credit Breakdown -->
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex items-center gap-4">
|
||||
<Skeleton
|
||||
v-if="isLoadingBalance"
|
||||
width="3rem"
|
||||
height="1rem"
|
||||
/>
|
||||
<div v-else class="text-sm text-text-secondary font-bold">
|
||||
${{ monthlyBonusCredits }}
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="text-sm text-text-secondary">
|
||||
{{ $t('subscription.monthlyBonusDescription') }}
|
||||
</div>
|
||||
<Button
|
||||
v-tooltip="$t('subscription.monthlyCreditsRollover')"
|
||||
icon="pi pi-question-circle"
|
||||
text
|
||||
rounded
|
||||
size="small"
|
||||
class="h-4 w-4"
|
||||
:pt="{
|
||||
icon: {
|
||||
class: 'text-text-secondary text-xs'
|
||||
}
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<Skeleton
|
||||
v-if="isLoadingBalance"
|
||||
width="3rem"
|
||||
height="1rem"
|
||||
/>
|
||||
<div v-else class="text-sm text-text-secondary font-bold">
|
||||
${{ prepaidCredits }}
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="text-sm text-text-secondary">
|
||||
{{ $t('subscription.prepaidDescription') }}
|
||||
</div>
|
||||
<Button
|
||||
v-tooltip="$t('subscription.prepaidCreditsInfo')"
|
||||
icon="pi pi-question-circle"
|
||||
text
|
||||
rounded
|
||||
size="small"
|
||||
class="h-4 w-4"
|
||||
:pt="{
|
||||
icon: {
|
||||
class: 'text-text-secondary text-xs'
|
||||
}
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<div class="text-xs text-muted">
|
||||
{{ $t('subscription.totalCredits') }}
|
||||
</div>
|
||||
<div class="text-2xl font-bold">${{ totalCredits }}</div>
|
||||
</div>
|
||||
<Button
|
||||
icon="pi pi-sync"
|
||||
severity="secondary"
|
||||
size="small"
|
||||
:loading="isLoadingBalance"
|
||||
@click="handleRefresh"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="latestEvents.length > 0"
|
||||
class="flex flex-col gap-2 pt-3 text-xs"
|
||||
>
|
||||
<div
|
||||
v-for="event in latestEvents"
|
||||
:key="event.event_id"
|
||||
class="flex items-center justify-between py-1"
|
||||
<a
|
||||
href="https://platform.comfy.org/profile/usage"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-sm text-text-secondary underline hover:text-text-secondary"
|
||||
style="text-decoration: underline"
|
||||
>
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<span class="font-medium">
|
||||
{{
|
||||
event.event_type
|
||||
? customerEventService.formatEventType(
|
||||
event.event_type
|
||||
)
|
||||
: ''
|
||||
}}
|
||||
</span>
|
||||
<span class="text-muted">
|
||||
{{
|
||||
event.createdAt
|
||||
? customerEventService.formatDate(event.createdAt)
|
||||
: ''
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="event.params?.amount !== undefined"
|
||||
class="font-bold"
|
||||
>
|
||||
${{
|
||||
customerEventService.formatAmount(
|
||||
event.params.amount as number
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between pt-2">
|
||||
<Button
|
||||
:label="$t('subscription.viewUsageHistory')"
|
||||
text
|
||||
severity="secondary"
|
||||
class="p-0 text-xs text-muted"
|
||||
@click="handleViewUsageHistory"
|
||||
/>
|
||||
{{ $t('subscription.viewUsageHistory') }}
|
||||
</a>
|
||||
<Button
|
||||
v-if="isActiveSubscription"
|
||||
:label="$t('subscription.addApiCredits')"
|
||||
:label="$t('subscription.addCredits')"
|
||||
severity="secondary"
|
||||
class="text-xs"
|
||||
class="p-2 min-h-8 bg-interface-menu-component-surface-selected"
|
||||
:pt="{
|
||||
root: {
|
||||
style: 'border-radius: 8px;'
|
||||
},
|
||||
label: {
|
||||
class: 'text-sm'
|
||||
}
|
||||
}"
|
||||
@click="handleAddApiCredits"
|
||||
/>
|
||||
</div>
|
||||
@@ -149,7 +211,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex flex-col gap-2 flex-1">
|
||||
<div class="text-sm">
|
||||
{{ $t('subscription.yourPlanIncludes') }}
|
||||
</div>
|
||||
@@ -161,7 +223,7 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex items-center justify-between border-t border-charcoal-400 pt-3"
|
||||
class="flex items-center justify-between border-t border-interface-stroke pt-3"
|
||||
>
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
@@ -170,7 +232,15 @@
|
||||
severity="secondary"
|
||||
icon="pi pi-question-circle"
|
||||
class="text-xs"
|
||||
@click="handleLearnMore"
|
||||
:pt="{
|
||||
label: {
|
||||
class: 'text-text-secondary'
|
||||
},
|
||||
icon: {
|
||||
class: 'text-text-secondary text-xs'
|
||||
}
|
||||
}"
|
||||
@click="handleLearnMoreClick"
|
||||
/>
|
||||
<Button
|
||||
:label="$t('subscription.messageSupport')"
|
||||
@@ -178,6 +248,15 @@
|
||||
severity="secondary"
|
||||
icon="pi pi-comment"
|
||||
class="text-xs"
|
||||
:loading="isLoadingSupport"
|
||||
:pt="{
|
||||
label: {
|
||||
class: 'text-text-secondary'
|
||||
},
|
||||
icon: {
|
||||
class: 'text-text-secondary text-xs'
|
||||
}
|
||||
}"
|
||||
@click="handleMessageSupport"
|
||||
/>
|
||||
</div>
|
||||
@@ -189,6 +268,14 @@
|
||||
icon="pi pi-external-link"
|
||||
icon-pos="right"
|
||||
class="text-xs"
|
||||
:pt="{
|
||||
label: {
|
||||
class: 'text-text-secondary'
|
||||
},
|
||||
icon: {
|
||||
class: 'text-text-secondary text-xs'
|
||||
}
|
||||
}"
|
||||
@click="handleInvoiceHistory"
|
||||
/>
|
||||
</div>
|
||||
@@ -198,26 +285,16 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import Skeleton from 'primevue/skeleton'
|
||||
import TabPanel from 'primevue/tabpanel'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
||||
import CloudBadge from '@/components/topbar/CloudBadge.vue'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import SubscribeButton from '@/platform/cloud/subscription/components/SubscribeButton.vue'
|
||||
import SubscriptionBenefits from '@/platform/cloud/subscription/components/SubscriptionBenefits.vue'
|
||||
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
||||
import type { AuditLog } from '@/services/customerEventsService'
|
||||
import { useCustomerEventsService } from '@/services/customerEventsService'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { formatMetronomeCurrency } from '@/utils/formatUtil'
|
||||
|
||||
const dialogService = useDialogService()
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const commandStore = useCommandStore()
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const customerEventService = useCustomerEventsService()
|
||||
import { useSubscriptionActions } from '@/platform/cloud/subscription/composables/useSubscriptionActions'
|
||||
import { useSubscriptionCredits } from '@/platform/cloud/subscription/composables/useSubscriptionCredits'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const {
|
||||
isActiveSubscription,
|
||||
@@ -226,54 +303,20 @@ const {
|
||||
formattedEndDate,
|
||||
formattedMonthlyPrice,
|
||||
manageSubscription,
|
||||
handleViewUsageHistory,
|
||||
handleLearnMore,
|
||||
handleInvoiceHistory,
|
||||
fetchStatus
|
||||
handleInvoiceHistory
|
||||
} = useSubscription()
|
||||
|
||||
const latestEvents = ref<AuditLog[]>([])
|
||||
const { totalCredits, monthlyBonusCredits, prepaidCredits, isLoadingBalance } =
|
||||
useSubscriptionCredits()
|
||||
|
||||
const totalCredits = computed(() => {
|
||||
if (!authStore.balance) return '0.00'
|
||||
return formatMetronomeCurrency(authStore.balance.amount_micros, 'usd')
|
||||
})
|
||||
|
||||
const isLoadingBalance = computed(() => authStore.isFetchingBalance)
|
||||
|
||||
const fetchLatestEvents = async () => {
|
||||
try {
|
||||
const response = await customerEventService.getMyEvents({
|
||||
page: 1,
|
||||
limit: 2
|
||||
})
|
||||
if (response?.events) {
|
||||
latestEvents.value = response.events
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[SubscriptionPanel] Error fetching latest events:', error)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void handleRefresh()
|
||||
})
|
||||
|
||||
const handleAddApiCredits = () => {
|
||||
dialogService.showTopUpCreditsDialog()
|
||||
}
|
||||
|
||||
const handleMessageSupport = async () => {
|
||||
await commandStore.execute('Comfy.ContactSupport')
|
||||
}
|
||||
|
||||
const handleRefresh = async () => {
|
||||
await Promise.all([
|
||||
authActions.fetchBalance(),
|
||||
fetchStatus(),
|
||||
fetchLatestEvents()
|
||||
])
|
||||
}
|
||||
const {
|
||||
isLoadingSupport,
|
||||
refreshTooltip,
|
||||
handleAddApiCredits,
|
||||
handleMessageSupport,
|
||||
handleRefresh,
|
||||
handleLearnMoreClick
|
||||
} = useSubscriptionActions()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
|
||||
/**
|
||||
* Composable for handling subscription panel actions and loading states
|
||||
*/
|
||||
export function useSubscriptionActions() {
|
||||
const { t } = useI18n()
|
||||
const dialogService = useDialogService()
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const commandStore = useCommandStore()
|
||||
const { fetchStatus, formattedRenewalDate } = useSubscription()
|
||||
|
||||
const isLoadingSupport = ref(false)
|
||||
|
||||
const refreshTooltip = computed(() => {
|
||||
const date =
|
||||
formattedRenewalDate.value || t('subscription.nextBillingCycle')
|
||||
return `Refreshes on ${date}`
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
void handleRefresh()
|
||||
})
|
||||
|
||||
const handleAddApiCredits = () => {
|
||||
dialogService.showTopUpCreditsDialog()
|
||||
}
|
||||
|
||||
const handleMessageSupport = async () => {
|
||||
try {
|
||||
isLoadingSupport.value = true
|
||||
await commandStore.execute('Comfy.ContactSupport')
|
||||
} catch (error) {
|
||||
console.error('[useSubscriptionActions] Error contacting support:', error)
|
||||
} finally {
|
||||
isLoadingSupport.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = async () => {
|
||||
try {
|
||||
await Promise.all([authActions.fetchBalance(), fetchStatus()])
|
||||
} catch (error) {
|
||||
console.error('[useSubscriptionActions] Error refreshing data:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleLearnMoreClick = () => {
|
||||
window.open('https://docs.comfy.org/get_started/cloud', '_blank')
|
||||
}
|
||||
|
||||
return {
|
||||
isLoadingSupport,
|
||||
refreshTooltip,
|
||||
handleAddApiCredits,
|
||||
handleMessageSupport,
|
||||
handleRefresh,
|
||||
handleLearnMoreClick
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { formatMetronomeCurrency } from '@/utils/formatUtil'
|
||||
|
||||
/**
|
||||
* Composable for handling subscription credit calculations and formatting
|
||||
*/
|
||||
export function useSubscriptionCredits() {
|
||||
const authStore = useFirebaseAuthStore()
|
||||
|
||||
const totalCredits = computed(() => {
|
||||
if (!authStore.balance?.amount_micros) return '0.00'
|
||||
try {
|
||||
return formatMetronomeCurrency(authStore.balance.amount_micros, 'usd')
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'[useSubscriptionCredits] Error formatting total credits:',
|
||||
error
|
||||
)
|
||||
return '0.00'
|
||||
}
|
||||
})
|
||||
|
||||
const monthlyBonusCredits = computed(() => {
|
||||
const balance = authStore.balance as any
|
||||
if (!balance?.cloud_credit_balance_micros) return '0.00'
|
||||
try {
|
||||
return formatMetronomeCurrency(balance.cloud_credit_balance_micros, 'usd')
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'[useSubscriptionCredits] Error formatting monthly bonus credits:',
|
||||
error
|
||||
)
|
||||
return '0.00'
|
||||
}
|
||||
})
|
||||
|
||||
const prepaidCredits = computed(() => {
|
||||
const balance = authStore.balance as any
|
||||
if (!balance?.prepaid_balance_micros) return '0.00'
|
||||
try {
|
||||
return formatMetronomeCurrency(balance.prepaid_balance_micros, 'usd')
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'[useSubscriptionCredits] Error formatting prepaid credits:',
|
||||
error
|
||||
)
|
||||
return '0.00'
|
||||
}
|
||||
})
|
||||
|
||||
const isLoadingBalance = computed(() => authStore.isFetchingBalance)
|
||||
|
||||
return {
|
||||
totalCredits,
|
||||
monthlyBonusCredits,
|
||||
prepaidCredits,
|
||||
isLoadingBalance
|
||||
}
|
||||
}
|
||||
@@ -69,6 +69,7 @@ export const useDialogService = () => {
|
||||
| 'server-config'
|
||||
| 'user'
|
||||
| 'credits'
|
||||
| 'subscription'
|
||||
) {
|
||||
const props = panel ? { props: { defaultPanel: panel } } : undefined
|
||||
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import SubscriptionPanel from '@/platform/cloud/subscription/components/SubscriptionPanel.vue'
|
||||
|
||||
// Mock composables
|
||||
const mockSubscriptionData = {
|
||||
isActiveSubscription: false,
|
||||
isCancelled: false,
|
||||
formattedRenewalDate: '2024-12-31',
|
||||
formattedEndDate: '2024-12-31',
|
||||
formattedMonthlyPrice: '$9.99',
|
||||
manageSubscription: vi.fn(),
|
||||
handleInvoiceHistory: vi.fn()
|
||||
}
|
||||
|
||||
const mockCreditsData = {
|
||||
totalCredits: '10.00',
|
||||
monthlyBonusCredits: '5.00',
|
||||
prepaidCredits: '5.00',
|
||||
isLoadingBalance: false
|
||||
}
|
||||
|
||||
const mockActionsData = {
|
||||
isLoadingSupport: false,
|
||||
refreshTooltip: 'Refreshes on 2024-12-31',
|
||||
handleAddApiCredits: vi.fn(),
|
||||
handleMessageSupport: vi.fn(),
|
||||
handleRefresh: vi.fn(),
|
||||
handleLearnMoreClick: vi.fn()
|
||||
}
|
||||
|
||||
vi.mock('@/platform/cloud/subscription/composables/useSubscription', () => ({
|
||||
useSubscription: () => mockSubscriptionData
|
||||
}))
|
||||
|
||||
vi.mock(
|
||||
'@/platform/cloud/subscription/composables/useSubscriptionCredits',
|
||||
() => ({
|
||||
useSubscriptionCredits: () => mockCreditsData
|
||||
})
|
||||
)
|
||||
|
||||
vi.mock(
|
||||
'@/platform/cloud/subscription/composables/useSubscriptionActions',
|
||||
() => ({
|
||||
useSubscriptionActions: () => mockActionsData
|
||||
})
|
||||
)
|
||||
|
||||
// Create i18n instance for testing
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: {
|
||||
en: {
|
||||
subscription: {
|
||||
title: 'Subscription',
|
||||
perMonth: '/ month',
|
||||
subscribeNow: 'Subscribe Now',
|
||||
manageSubscription: 'Manage Subscription',
|
||||
partnerNodesBalance: 'Partner Nodes Balance',
|
||||
partnerNodesDescription: 'Credits for partner nodes',
|
||||
totalCredits: 'Total Credits',
|
||||
monthlyBonusDescription: 'Monthly bonus',
|
||||
prepaidDescription: 'Prepaid credits',
|
||||
monthlyCreditsRollover: 'Monthly credits rollover info',
|
||||
prepaidCreditsInfo: 'Prepaid credits info',
|
||||
viewUsageHistory: 'View Usage History',
|
||||
addCredits: 'Add Credits',
|
||||
yourPlanIncludes: 'Your plan includes',
|
||||
learnMore: 'Learn More',
|
||||
messageSupport: 'Message Support',
|
||||
invoiceHistory: 'Invoice History',
|
||||
renewsDate: 'Renews {date}',
|
||||
expiresDate: 'Expires {date}'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function createWrapper(overrides = {}) {
|
||||
return mount(SubscriptionPanel, {
|
||||
global: {
|
||||
plugins: [createTestingPinia({ createSpy: vi.fn }), i18n],
|
||||
stubs: {
|
||||
CloudBadge: true,
|
||||
SubscribeButton: true,
|
||||
SubscriptionBenefits: true,
|
||||
Button: {
|
||||
template:
|
||||
'<button @click="$emit(\'click\')" :disabled="loading" :data-testid="label" :data-icon="icon">{{ label }}</button>',
|
||||
props: [
|
||||
'loading',
|
||||
'label',
|
||||
'icon',
|
||||
'text',
|
||||
'severity',
|
||||
'size',
|
||||
'iconPos',
|
||||
'pt'
|
||||
],
|
||||
emits: ['click']
|
||||
},
|
||||
Skeleton: {
|
||||
template: '<div class="skeleton"></div>'
|
||||
}
|
||||
}
|
||||
},
|
||||
...overrides
|
||||
})
|
||||
}
|
||||
|
||||
describe('SubscriptionPanel', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('subscription state functionality', () => {
|
||||
it('shows correct UI for active subscription', () => {
|
||||
mockSubscriptionData.isActiveSubscription = true
|
||||
const wrapper = createWrapper()
|
||||
expect(wrapper.text()).toContain('Manage Subscription')
|
||||
expect(wrapper.text()).toContain('Add Credits')
|
||||
})
|
||||
|
||||
it('shows correct UI for inactive subscription', () => {
|
||||
mockSubscriptionData.isActiveSubscription = false
|
||||
const wrapper = createWrapper()
|
||||
expect(wrapper.findComponent({ name: 'SubscribeButton' }).exists()).toBe(
|
||||
true
|
||||
)
|
||||
expect(wrapper.text()).not.toContain('Manage Subscription')
|
||||
expect(wrapper.text()).not.toContain('Add Credits')
|
||||
})
|
||||
|
||||
it('shows renewal date for active non-cancelled subscription', () => {
|
||||
mockSubscriptionData.isActiveSubscription = true
|
||||
mockSubscriptionData.isCancelled = false
|
||||
const wrapper = createWrapper()
|
||||
expect(wrapper.text()).toContain('Renews 2024-12-31')
|
||||
})
|
||||
|
||||
it('shows expiry date for cancelled subscription', () => {
|
||||
mockSubscriptionData.isActiveSubscription = true
|
||||
mockSubscriptionData.isCancelled = true
|
||||
const wrapper = createWrapper()
|
||||
expect(wrapper.text()).toContain('Expires 2024-12-31')
|
||||
})
|
||||
})
|
||||
|
||||
describe('credit display functionality', () => {
|
||||
it('displays dynamic credit values correctly', () => {
|
||||
const wrapper = createWrapper()
|
||||
expect(wrapper.text()).toContain('$10.00') // totalCredits
|
||||
expect(wrapper.text()).toContain('$5.00') // both monthlyBonus and prepaid
|
||||
})
|
||||
|
||||
it('shows loading skeleton when fetching balance', () => {
|
||||
mockCreditsData.isLoadingBalance = true
|
||||
const wrapper = createWrapper()
|
||||
expect(wrapper.findAll('.skeleton').length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('hides skeleton when balance loaded', () => {
|
||||
mockCreditsData.isLoadingBalance = false
|
||||
const wrapper = createWrapper()
|
||||
expect(wrapper.findAll('.skeleton').length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('action buttons', () => {
|
||||
it('should call handleLearnMoreClick when learn more is clicked', async () => {
|
||||
const wrapper = createWrapper()
|
||||
const learnMoreButton = wrapper.find('[data-testid="Learn More"]')
|
||||
await learnMoreButton.trigger('click')
|
||||
expect(mockActionsData.handleLearnMoreClick).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('should call handleMessageSupport when message support is clicked', async () => {
|
||||
const wrapper = createWrapper()
|
||||
const supportButton = wrapper.find('[data-testid="Message Support"]')
|
||||
await supportButton.trigger('click')
|
||||
expect(mockActionsData.handleMessageSupport).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('should call handleRefresh when refresh button is clicked', async () => {
|
||||
const wrapper = createWrapper()
|
||||
// Find the refresh button by icon
|
||||
const refreshButton = wrapper.find('[data-icon="pi pi-sync"]')
|
||||
await refreshButton.trigger('click')
|
||||
expect(mockActionsData.handleRefresh).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
|
||||
describe('loading states', () => {
|
||||
it('should show loading state on support button when loading', () => {
|
||||
mockActionsData.isLoadingSupport = true
|
||||
const wrapper = createWrapper()
|
||||
const supportButton = wrapper.find('[data-testid="Message Support"]')
|
||||
expect(supportButton.attributes('disabled')).toBeDefined()
|
||||
})
|
||||
|
||||
it('should show loading state on refresh button when loading balance', () => {
|
||||
mockCreditsData.isLoadingBalance = true
|
||||
const wrapper = createWrapper()
|
||||
const refreshButton = wrapper.find('[data-icon="pi pi-sync"]')
|
||||
expect(refreshButton.attributes('disabled')).toBeDefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,137 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useSubscriptionActions } from '@/platform/cloud/subscription/composables/useSubscriptionActions'
|
||||
|
||||
// Mock dependencies
|
||||
const mockFetchBalance = vi.fn()
|
||||
const mockFetchStatus = vi.fn()
|
||||
const mockShowTopUpCreditsDialog = vi.fn()
|
||||
const mockExecute = vi.fn()
|
||||
const mockT = vi.fn((key: string) => {
|
||||
if (key === 'subscription.nextBillingCycle') return 'next billing cycle'
|
||||
return key
|
||||
})
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: mockT
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/auth/useFirebaseAuthActions', () => ({
|
||||
useFirebaseAuthActions: () => ({
|
||||
fetchBalance: mockFetchBalance
|
||||
})
|
||||
}))
|
||||
|
||||
const mockFormattedRenewalDate = { value: '2024-12-31' }
|
||||
|
||||
vi.mock('@/platform/cloud/subscription/composables/useSubscription', () => ({
|
||||
useSubscription: () => ({
|
||||
fetchStatus: mockFetchStatus,
|
||||
formattedRenewalDate: mockFormattedRenewalDate
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/services/dialogService', () => ({
|
||||
useDialogService: () => ({
|
||||
showTopUpCreditsDialog: mockShowTopUpCreditsDialog
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/commandStore', () => ({
|
||||
useCommandStore: () => ({
|
||||
execute: mockExecute
|
||||
})
|
||||
}))
|
||||
|
||||
// Mock window.open
|
||||
const mockOpen = vi.fn()
|
||||
Object.defineProperty(window, 'open', {
|
||||
writable: true,
|
||||
value: mockOpen
|
||||
})
|
||||
|
||||
describe('useSubscriptionActions', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockFormattedRenewalDate.value = '2024-12-31'
|
||||
})
|
||||
|
||||
describe('refreshTooltip', () => {
|
||||
it('should format tooltip with renewal date', () => {
|
||||
const { refreshTooltip } = useSubscriptionActions()
|
||||
expect(refreshTooltip.value).toBe('Refreshes on 2024-12-31')
|
||||
})
|
||||
|
||||
it('should use fallback text when no renewal date', () => {
|
||||
mockFormattedRenewalDate.value = ''
|
||||
const { refreshTooltip } = useSubscriptionActions()
|
||||
expect(refreshTooltip.value).toBe('Refreshes on next billing cycle')
|
||||
expect(mockT).toHaveBeenCalledWith('subscription.nextBillingCycle')
|
||||
})
|
||||
})
|
||||
|
||||
describe('handleAddApiCredits', () => {
|
||||
it('should call showTopUpCreditsDialog', () => {
|
||||
const { handleAddApiCredits } = useSubscriptionActions()
|
||||
handleAddApiCredits()
|
||||
expect(mockShowTopUpCreditsDialog).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
|
||||
describe('handleMessageSupport', () => {
|
||||
it('should execute support command and manage loading state', async () => {
|
||||
const { handleMessageSupport, isLoadingSupport } =
|
||||
useSubscriptionActions()
|
||||
|
||||
expect(isLoadingSupport.value).toBe(false)
|
||||
|
||||
const promise = handleMessageSupport()
|
||||
expect(isLoadingSupport.value).toBe(true)
|
||||
|
||||
await promise
|
||||
expect(mockExecute).toHaveBeenCalledWith('Comfy.ContactSupport')
|
||||
expect(isLoadingSupport.value).toBe(false)
|
||||
})
|
||||
|
||||
it('should handle errors gracefully', async () => {
|
||||
mockExecute.mockRejectedValueOnce(new Error('Command failed'))
|
||||
const { handleMessageSupport, isLoadingSupport } =
|
||||
useSubscriptionActions()
|
||||
|
||||
await handleMessageSupport()
|
||||
expect(isLoadingSupport.value).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('handleRefresh', () => {
|
||||
it('should call both fetchBalance and fetchStatus', async () => {
|
||||
const { handleRefresh } = useSubscriptionActions()
|
||||
await handleRefresh()
|
||||
|
||||
expect(mockFetchBalance).toHaveBeenCalledOnce()
|
||||
expect(mockFetchStatus).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('should handle errors gracefully', async () => {
|
||||
mockFetchBalance.mockRejectedValueOnce(new Error('Fetch failed'))
|
||||
const { handleRefresh } = useSubscriptionActions()
|
||||
|
||||
// Should not throw
|
||||
await expect(handleRefresh()).resolves.toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('handleLearnMoreClick', () => {
|
||||
it('should open learn more URL', () => {
|
||||
const { handleLearnMoreClick } = useSubscriptionActions()
|
||||
handleLearnMoreClick()
|
||||
|
||||
expect(mockOpen).toHaveBeenCalledWith(
|
||||
'https://docs.comfy.org/get_started/cloud',
|
||||
'_blank'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,146 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useSubscriptionCredits } from '@/platform/cloud/subscription/composables/useSubscriptionCredits'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
|
||||
// Mock Firebase Auth and related modules
|
||||
vi.mock('vuefire', () => ({
|
||||
useFirebaseAuth: vi.fn(() => ({
|
||||
onAuthStateChanged: vi.fn(),
|
||||
setPersistence: vi.fn()
|
||||
}))
|
||||
}))
|
||||
|
||||
vi.mock('firebase/auth', () => ({
|
||||
onAuthStateChanged: vi.fn(() => {
|
||||
// Mock the callback to be called immediately for testing
|
||||
return vi.fn()
|
||||
}),
|
||||
onIdTokenChanged: vi.fn(),
|
||||
setPersistence: vi.fn().mockResolvedValue(undefined),
|
||||
browserLocalPersistence: {},
|
||||
GoogleAuthProvider: class {
|
||||
addScope = vi.fn()
|
||||
setCustomParameters = vi.fn()
|
||||
},
|
||||
GithubAuthProvider: class {
|
||||
addScope = vi.fn()
|
||||
setCustomParameters = vi.fn()
|
||||
}
|
||||
}))
|
||||
|
||||
// Mock other dependencies
|
||||
vi.mock('@/services/dialogService', () => ({
|
||||
useDialogService: () => ({
|
||||
showDialog: vi.fn()
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/telemetry', () => ({
|
||||
useTelemetry: () => ({
|
||||
track: vi.fn()
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/toastStore', () => ({
|
||||
useToastStore: () => ({
|
||||
add: vi.fn()
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/apiKeyAuthStore', () => ({
|
||||
useApiKeyAuthStore: () => ({
|
||||
headers: {}
|
||||
})
|
||||
}))
|
||||
|
||||
// Mock formatMetronomeCurrency
|
||||
vi.mock('@/utils/formatUtil', () => ({
|
||||
formatMetronomeCurrency: vi.fn((micros: number) => {
|
||||
// Simple mock that converts micros to dollars
|
||||
return (micros / 1000000).toFixed(2)
|
||||
})
|
||||
}))
|
||||
|
||||
describe('useSubscriptionCredits', () => {
|
||||
let authStore: ReturnType<typeof useFirebaseAuthStore>
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
authStore = useFirebaseAuthStore()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('totalCredits', () => {
|
||||
it('should return "0.00" when balance is null', () => {
|
||||
authStore.balance = null
|
||||
const { totalCredits } = useSubscriptionCredits()
|
||||
expect(totalCredits.value).toBe('0.00')
|
||||
})
|
||||
|
||||
it('should return "0.00" when amount_micros is missing', () => {
|
||||
authStore.balance = {} as any
|
||||
const { totalCredits } = useSubscriptionCredits()
|
||||
expect(totalCredits.value).toBe('0.00')
|
||||
})
|
||||
|
||||
it('should format amount_micros correctly', () => {
|
||||
authStore.balance = { amount_micros: 5000000 } as any
|
||||
const { totalCredits } = useSubscriptionCredits()
|
||||
expect(totalCredits.value).toBe('5.00')
|
||||
})
|
||||
|
||||
it('should handle formatting errors gracefully', async () => {
|
||||
const mockFormatMetronomeCurrency = vi.mocked(
|
||||
await import('@/utils/formatUtil')
|
||||
).formatMetronomeCurrency
|
||||
mockFormatMetronomeCurrency.mockImplementationOnce(() => {
|
||||
throw new Error('Formatting error')
|
||||
})
|
||||
|
||||
authStore.balance = { amount_micros: 5000000 } as any
|
||||
const { totalCredits } = useSubscriptionCredits()
|
||||
expect(totalCredits.value).toBe('0.00')
|
||||
})
|
||||
})
|
||||
|
||||
describe('monthlyBonusCredits', () => {
|
||||
it('should return "0.00" when cloud_credit_balance_micros is missing', () => {
|
||||
authStore.balance = {} as any
|
||||
const { monthlyBonusCredits } = useSubscriptionCredits()
|
||||
expect(monthlyBonusCredits.value).toBe('0.00')
|
||||
})
|
||||
|
||||
it('should format cloud_credit_balance_micros correctly', () => {
|
||||
authStore.balance = { cloud_credit_balance_micros: 2500000 } as any
|
||||
const { monthlyBonusCredits } = useSubscriptionCredits()
|
||||
expect(monthlyBonusCredits.value).toBe('2.50')
|
||||
})
|
||||
})
|
||||
|
||||
describe('prepaidCredits', () => {
|
||||
it('should return "0.00" when prepaid_balance_micros is missing', () => {
|
||||
authStore.balance = {} as any
|
||||
const { prepaidCredits } = useSubscriptionCredits()
|
||||
expect(prepaidCredits.value).toBe('0.00')
|
||||
})
|
||||
|
||||
it('should format prepaid_balance_micros correctly', () => {
|
||||
authStore.balance = { prepaid_balance_micros: 7500000 } as any
|
||||
const { prepaidCredits } = useSubscriptionCredits()
|
||||
expect(prepaidCredits.value).toBe('7.50')
|
||||
})
|
||||
})
|
||||
|
||||
describe('isLoadingBalance', () => {
|
||||
it('should reflect authStore.isFetchingBalance', () => {
|
||||
authStore.isFetchingBalance = true
|
||||
const { isLoadingBalance } = useSubscriptionCredits()
|
||||
expect(isLoadingBalance.value).toBe(true)
|
||||
|
||||
authStore.isFetchingBalance = false
|
||||
expect(isLoadingBalance.value).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user