Skip to content

Commit 8558f87

Browse files
christian-byrnegithub-actionsChenlei Hu
authored
[API Node] User management (#3567)
Co-authored-by: github-actions <[email protected]> Co-authored-by: Chenlei Hu <[email protected]>
1 parent 262991d commit 8558f87

File tree

22 files changed

+1172
-153
lines changed

22 files changed

+1172
-153
lines changed

src/components/dialog/content/SettingDialogContent.vue

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
</PanelTemplate>
4949

5050
<AboutPanel />
51+
<UserPanel />
5152
<CreditsPanel />
5253
<Suspense>
5354
<KeybindingPanel />
@@ -96,9 +97,16 @@ import CurrentUserMessage from './setting/CurrentUserMessage.vue'
9697
import FirstTimeUIMessage from './setting/FirstTimeUIMessage.vue'
9798
import PanelTemplate from './setting/PanelTemplate.vue'
9899
import SettingsPanel from './setting/SettingsPanel.vue'
100+
import UserPanel from './setting/UserPanel.vue'
99101
100102
const { defaultPanel } = defineProps<{
101-
defaultPanel?: 'about' | 'keybinding' | 'extension' | 'server-config'
103+
defaultPanel?:
104+
| 'about'
105+
| 'keybinding'
106+
| 'extension'
107+
| 'server-config'
108+
| 'user'
109+
| 'credits'
102110
}>()
103111
104112
const KeybindingPanel = defineAsyncComponent(

src/components/dialog/content/SignInContent.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@
7070
</a>
7171
{{ t('auth.login.andText') }}
7272
<a
73-
href="https://www.comfy.org/privacy-policy"
73+
href="https://www.comfy.org/privacy"
7474
target="_blank"
7575
class="text-blue-500 cursor-pointer"
7676
>
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<template>
2+
<div class="flex flex-col items-center gap-4 p-4">
3+
<div class="flex flex-col items-center text-center">
4+
<i class="pi pi-exclamation-circle mb-4" style="font-size: 2rem" />
5+
<h2 class="text-2xl font-semibold mb-2">
6+
{{ $t(`auth.required.${type}.title`) }}
7+
</h2>
8+
<p class="text-gray-600 mb-4 max-w-md">
9+
{{ $t(`auth.required.${type}.message`) }}
10+
</p>
11+
</div>
12+
<div class="flex gap-4">
13+
<Button
14+
class="w-60"
15+
severity="primary"
16+
:label="$t(`auth.required.${type}.action`)"
17+
@click="openPanel"
18+
/>
19+
</div>
20+
</div>
21+
</template>
22+
23+
<script setup lang="ts">
24+
import Button from 'primevue/button'
25+
26+
import { useDialogStore } from '@/stores/dialogStore'
27+
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
28+
29+
const props = defineProps<{
30+
type: 'signIn' | 'credits'
31+
}>()
32+
33+
const dialogStore = useDialogStore()
34+
const authStore = useFirebaseAuthStore()
35+
36+
const openPanel = () => {
37+
// Close the current dialog
38+
dialogStore.closeDialog({ key: 'signin-required' })
39+
40+
// Open user settings and navigate to appropriate panel
41+
if (props.type === 'credits') {
42+
authStore.openCreditsPanel()
43+
} else {
44+
authStore.openSignInPanel()
45+
}
46+
}
47+
</script>
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
<template>
2+
<div class="flex flex-col p-6">
3+
<div
4+
class="flex items-center gap-2"
5+
:class="{ 'text-red-500': isInsufficientCredits }"
6+
>
7+
<i
8+
:class="[
9+
'text-2xl',
10+
isInsufficientCredits ? 'pi pi-exclamation-triangle' : ''
11+
]"
12+
/>
13+
<h2 class="text-2xl font-semibold">
14+
{{
15+
$t(
16+
isInsufficientCredits
17+
? 'credits.topUp.insufficientTitle'
18+
: 'credits.topUp.title'
19+
)
20+
}}
21+
</h2>
22+
</div>
23+
24+
<!-- Error Message -->
25+
<p v-if="isInsufficientCredits" class="text-lg text-muted mt-6">
26+
{{ $t('credits.topUp.insufficientMessage') }}
27+
</p>
28+
29+
<!-- Balance Section -->
30+
<div class="flex justify-between items-center mt-8">
31+
<div class="flex flex-col gap-2">
32+
<div class="text-muted">{{ $t('credits.yourCreditBalance') }}</div>
33+
<div class="flex items-center gap-2">
34+
<Tag
35+
severity="secondary"
36+
icon="pi pi-dollar"
37+
rounded
38+
class="text-amber-400 p-1"
39+
/>
40+
<span class="text-2xl">{{ formattedBalance }}</span>
41+
</div>
42+
</div>
43+
<Button
44+
text
45+
severity="secondary"
46+
:label="$t('credits.creditsHistory')"
47+
icon="pi pi-arrow-up-right"
48+
@click="handleSeeDetails"
49+
/>
50+
</div>
51+
52+
<!-- Amount Input Section -->
53+
<div class="flex flex-col gap-2 mt-8">
54+
<div>
55+
<span class="text-muted">{{ $t('credits.topUp.addCredits') }}</span>
56+
<span class="text-muted text-sm ml-1">{{
57+
$t('credits.topUp.maxAmount')
58+
}}</span>
59+
</div>
60+
<div class="flex items-center gap-2">
61+
<Tag
62+
severity="secondary"
63+
icon="pi pi-dollar"
64+
rounded
65+
class="text-amber-400 p-1"
66+
/>
67+
<InputNumber
68+
v-model="amount"
69+
:min="1"
70+
:max="1000"
71+
:step="1"
72+
mode="currency"
73+
currency="USD"
74+
show-buttons
75+
@blur="handleBlur"
76+
@input="handleInput"
77+
/>
78+
</div>
79+
</div>
80+
<div class="flex justify-end mt-8">
81+
<ProgressSpinner v-if="loading" class="w-8 h-8" />
82+
<Button
83+
v-else
84+
severity="primary"
85+
:label="$t('credits.topUp.buyNow')"
86+
:disabled="!amount || amount > 1000"
87+
:pt="{
88+
root: { class: 'px-8' }
89+
}"
90+
@click="handleBuyNow"
91+
/>
92+
</div>
93+
</div>
94+
</template>
95+
96+
<script setup lang="ts">
97+
import Button from 'primevue/button'
98+
import InputNumber from 'primevue/inputnumber'
99+
import ProgressSpinner from 'primevue/progressspinner'
100+
import Tag from 'primevue/tag'
101+
import { computed, onBeforeUnmount, ref } from 'vue'
102+
103+
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
104+
import { formatMetronomeCurrency, usdToMicros } from '@/utils/formatUtil'
105+
106+
defineProps<{
107+
isInsufficientCredits?: boolean
108+
}>()
109+
110+
const authStore = useFirebaseAuthStore()
111+
const amount = ref<number>(9.99)
112+
const didClickBuyNow = ref(false)
113+
const loading = computed(() => authStore.loading)
114+
115+
const handleBlur = (e: any) => {
116+
if (e.target.value) {
117+
amount.value = parseFloat(e.target.value)
118+
}
119+
}
120+
121+
const handleInput = (e: any) => {
122+
amount.value = e.value
123+
}
124+
125+
const formattedBalance = computed(() => {
126+
if (!authStore.balance) return '0.000'
127+
return formatMetronomeCurrency(authStore.balance.amount_micros, 'usd')
128+
})
129+
130+
const handleSeeDetails = async () => {
131+
const response = await authStore.accessBillingPortal()
132+
if (!response?.billing_portal_url) return
133+
window.open(response.billing_portal_url, '_blank')
134+
}
135+
136+
const handleBuyNow = async () => {
137+
if (!amount.value) return
138+
139+
const response = await authStore.initiateCreditPurchase({
140+
amount_micros: usdToMicros(amount.value),
141+
currency: 'usd'
142+
})
143+
144+
if (!response?.checkout_url) return
145+
146+
didClickBuyNow.value = true
147+
148+
// Go to Stripe checkout page
149+
window.open(response.checkout_url, '_blank')
150+
}
151+
152+
onBeforeUnmount(() => {
153+
if (didClickBuyNow.value) {
154+
// If clicked buy now, then returned back to the dialog and closed, fetch the balance
155+
void authStore.fetchBalance()
156+
}
157+
})
158+
</script>

0 commit comments

Comments
 (0)