mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-06-25 09:09:14 +00:00
Compare commits
11 Commits
codex/cove
...
mattmiller
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f42ce95a7 | ||
|
|
76993b21c0 | ||
|
|
737c7c2190 | ||
|
|
f528b7cd1a | ||
|
|
44e37ee53d | ||
|
|
1fee4490d4 | ||
|
|
8d23daa33d | ||
|
|
880582ab5d | ||
|
|
e498c4ae0d | ||
|
|
4cadbb8af9 | ||
|
|
cc048464fa |
@@ -104,6 +104,8 @@ vi.mock('@/stores/authStore', () => ({
|
||||
useAuthStore: () =>
|
||||
reactive({
|
||||
getAuthHeader: mockGetAuthHeader,
|
||||
fetchWithCustomerRecovery: (input: string, init?: RequestInit) =>
|
||||
fetch(input, init),
|
||||
userId: computed(() => mockUserId.value)
|
||||
}),
|
||||
AuthStoreError: class extends Error {}
|
||||
|
||||
@@ -143,6 +143,8 @@ vi.mock('@/services/dialogService', () => ({
|
||||
vi.mock('@/stores/authStore', () => ({
|
||||
useAuthStore: vi.fn(() => ({
|
||||
getAuthHeader: mockGetAuthHeader,
|
||||
fetchWithCustomerRecovery: (input: string, init?: RequestInit) =>
|
||||
fetch(input, init),
|
||||
get isInitialized() {
|
||||
return mockAuthStoreInitialized.value
|
||||
},
|
||||
|
||||
@@ -53,7 +53,7 @@ function useSubscriptionInternal() {
|
||||
const { showSubscriptionRequiredDialog } = useDialogService()
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const { getAuthHeader } = authStore
|
||||
const { getAuthHeader, fetchWithCustomerRecovery } = authStore
|
||||
const { wrapWithErrorHandlingAsync } = useErrorHandling()
|
||||
|
||||
const { isLoggedIn } = useCurrentUser()
|
||||
@@ -326,7 +326,7 @@ function useSubscriptionInternal() {
|
||||
async function fetchSubscriptionStatus(): Promise<CloudSubscriptionStatusResponse | null> {
|
||||
const headers = await buildAuthHeaders()
|
||||
|
||||
const response = await fetch(
|
||||
const response = await fetchWithCustomerRecovery(
|
||||
buildApiUrl('/customers/cloud-subscription-status'),
|
||||
{
|
||||
headers
|
||||
@@ -416,7 +416,7 @@ function useSubscriptionInternal() {
|
||||
const headers = await buildAuthHeaders()
|
||||
const checkoutAttribution = await getCheckoutAttributionForCloud()
|
||||
|
||||
const response = await fetch(
|
||||
const response = await fetchWithCustomerRecovery(
|
||||
buildApiUrl('/customers/cloud-subscription-checkout'),
|
||||
{
|
||||
method: 'POST',
|
||||
|
||||
@@ -71,6 +71,8 @@ vi.mock('@/stores/authStore', () => ({
|
||||
useAuthStore: vi.fn(() =>
|
||||
reactive({
|
||||
getAuthHeader: mockGetAuthHeader,
|
||||
fetchWithCustomerRecovery: (input: string, init?: RequestInit) =>
|
||||
fetch(input, init),
|
||||
userId: computed(() => mockUserId.value)
|
||||
})
|
||||
),
|
||||
|
||||
@@ -70,7 +70,7 @@ export async function performSubscriptionCheckout(
|
||||
}
|
||||
const checkoutPayload = { ...checkoutAttribution }
|
||||
|
||||
const response = await fetch(
|
||||
const response = await authStore.fetchWithCustomerRecovery(
|
||||
`${getComfyApiBaseUrl()}/customers/cloud-subscription-checkout/${checkoutTier}`,
|
||||
{
|
||||
method: 'POST',
|
||||
|
||||
@@ -859,4 +859,330 @@ describe('useAuthStore', () => {
|
||||
await expect(store.createCustomer()).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('fetchWithCustomerRecovery', () => {
|
||||
const make409 = (message: string) => {
|
||||
const body = { message }
|
||||
return {
|
||||
ok: false,
|
||||
status: 409,
|
||||
statusText: 'Conflict',
|
||||
json: () => Promise.resolve(body),
|
||||
clone: () => ({ json: () => Promise.resolve(body) })
|
||||
}
|
||||
}
|
||||
const makeConflictResponse = () => make409('Failed to find customer')
|
||||
|
||||
const countCustomerPosts = () =>
|
||||
mockFetch.mock.calls.filter(
|
||||
([url, init]) =>
|
||||
typeof url === 'string' &&
|
||||
url.endsWith('/customers') &&
|
||||
(init as RequestInit | undefined)?.method === 'POST'
|
||||
).length
|
||||
|
||||
it('should provision the customer and retry once when a /customers/* call returns 409', async () => {
|
||||
let balanceCalls = 0
|
||||
mockFetch.mockImplementation((url: string, init?: RequestInit) => {
|
||||
if (url.endsWith('/customers') && init?.method === 'POST') {
|
||||
return Promise.resolve(mockCreateCustomerResponse)
|
||||
}
|
||||
if (url.endsWith('/customers/balance')) {
|
||||
balanceCalls++
|
||||
return Promise.resolve(
|
||||
balanceCalls === 1
|
||||
? makeConflictResponse()
|
||||
: mockFetchBalanceResponse
|
||||
)
|
||||
}
|
||||
return Promise.reject(new Error('Unexpected API call'))
|
||||
})
|
||||
|
||||
const result = await store.fetchBalance()
|
||||
|
||||
expect(result).toEqual({ balance: 0 })
|
||||
expect(balanceCalls).toBe(2)
|
||||
expect(countCustomerPosts()).toBe(1)
|
||||
})
|
||||
|
||||
it('should deduplicate concurrent recovery attempts into a single customer creation', async () => {
|
||||
const seenUrls = new Set<string>()
|
||||
mockFetch.mockImplementation((url: string, init?: RequestInit) => {
|
||||
if (url.endsWith('/customers') && init?.method === 'POST') {
|
||||
return Promise.resolve(mockCreateCustomerResponse)
|
||||
}
|
||||
if (!seenUrls.has(url)) {
|
||||
seenUrls.add(url)
|
||||
return Promise.resolve(makeConflictResponse())
|
||||
}
|
||||
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) })
|
||||
})
|
||||
|
||||
const [first, second] = await Promise.all([
|
||||
store.fetchWithCustomerRecovery('https://api.test/customers/balance'),
|
||||
store.fetchWithCustomerRecovery(
|
||||
'https://api.test/customers/cloud-subscription-status'
|
||||
)
|
||||
])
|
||||
|
||||
expect(first.ok).toBe(true)
|
||||
expect(second.ok).toBe(true)
|
||||
expect(countCustomerPosts()).toBe(1)
|
||||
})
|
||||
|
||||
it('should not provision the customer again after a successful recovery', async () => {
|
||||
mockFetch.mockImplementation((url: string, init?: RequestInit) => {
|
||||
if (url.endsWith('/customers') && init?.method === 'POST') {
|
||||
return Promise.resolve(mockCreateCustomerResponse)
|
||||
}
|
||||
// Endpoint keeps conflicting even after recovery succeeds
|
||||
return Promise.resolve(makeConflictResponse())
|
||||
})
|
||||
|
||||
const first = await store.fetchWithCustomerRecovery(
|
||||
'https://api.test/customers/balance'
|
||||
)
|
||||
const second = await store.fetchWithCustomerRecovery(
|
||||
'https://api.test/customers/balance'
|
||||
)
|
||||
|
||||
expect(first.status).toBe(409)
|
||||
expect(second.status).toBe(409)
|
||||
expect(countCustomerPosts()).toBe(1)
|
||||
})
|
||||
|
||||
it('should return the original 409 response when customer provisioning fails', async () => {
|
||||
mockFetch.mockImplementation((url: string, init?: RequestInit) => {
|
||||
if (url.endsWith('/customers') && init?.method === 'POST') {
|
||||
return Promise.resolve({
|
||||
ok: false,
|
||||
status: 500,
|
||||
statusText: 'Internal Server Error'
|
||||
})
|
||||
}
|
||||
return Promise.resolve(makeConflictResponse())
|
||||
})
|
||||
|
||||
const response = await store.fetchWithCustomerRecovery(
|
||||
'https://api.test/customers/balance'
|
||||
)
|
||||
|
||||
expect(response.status).toBe(409)
|
||||
expect(countCustomerPosts()).toBe(1)
|
||||
})
|
||||
|
||||
it('should pass through non-409 responses without provisioning', async () => {
|
||||
mockFetch.mockImplementation(() =>
|
||||
Promise.resolve({ ok: true, json: () => Promise.resolve({}) })
|
||||
)
|
||||
|
||||
const response = await store.fetchWithCustomerRecovery(
|
||||
'https://api.test/customers/balance'
|
||||
)
|
||||
|
||||
expect(response.ok).toBe(true)
|
||||
expect(countCustomerPosts()).toBe(0)
|
||||
})
|
||||
|
||||
it('should not provision for a 409 that is not a missing-customer conflict', async () => {
|
||||
mockFetch.mockImplementation(() =>
|
||||
Promise.resolve(make409('Subscription already active'))
|
||||
)
|
||||
|
||||
const response = await store.fetchWithCustomerRecovery(
|
||||
'https://api.test/customers/cloud-subscription-checkout/standard',
|
||||
{ method: 'POST' }
|
||||
)
|
||||
|
||||
expect(response.status).toBe(409)
|
||||
expect(countCustomerPosts()).toBe(0)
|
||||
})
|
||||
|
||||
it('should not provision for a 409 from a non-customer endpoint', async () => {
|
||||
mockFetch.mockImplementation(() =>
|
||||
Promise.resolve(makeConflictResponse())
|
||||
)
|
||||
|
||||
const response = await store.fetchWithCustomerRecovery(
|
||||
'https://api.test/workflows'
|
||||
)
|
||||
|
||||
expect(response.status).toBe(409)
|
||||
expect(countCustomerPosts()).toBe(0)
|
||||
})
|
||||
|
||||
it('should re-provision after the auth state changes to a different session', async () => {
|
||||
mockFetch.mockImplementation((url: string, init?: RequestInit) => {
|
||||
if (url.endsWith('/customers') && init?.method === 'POST') {
|
||||
return Promise.resolve(mockCreateCustomerResponse)
|
||||
}
|
||||
return Promise.resolve(makeConflictResponse())
|
||||
})
|
||||
|
||||
await store.fetchWithCustomerRecovery(
|
||||
'https://api.test/customers/balance'
|
||||
)
|
||||
expect(countCustomerPosts()).toBe(1)
|
||||
|
||||
// Sign out, then a different account signs in: the memoized recovery
|
||||
// from the previous account must not short-circuit the new one.
|
||||
authStateCallback(null)
|
||||
authStateCallback(mockUser)
|
||||
|
||||
await store.fetchWithCustomerRecovery(
|
||||
'https://api.test/customers/balance'
|
||||
)
|
||||
expect(countCustomerPosts()).toBe(2)
|
||||
})
|
||||
|
||||
it('should return the original 409 when the retry fails at the network level', async () => {
|
||||
let balanceCalls = 0
|
||||
mockFetch.mockImplementation((url: string, init?: RequestInit) => {
|
||||
if (url.endsWith('/customers') && init?.method === 'POST') {
|
||||
return Promise.resolve(mockCreateCustomerResponse)
|
||||
}
|
||||
balanceCalls++
|
||||
return balanceCalls === 1
|
||||
? Promise.resolve(makeConflictResponse())
|
||||
: Promise.reject(new TypeError('network down'))
|
||||
})
|
||||
|
||||
const response = await store.fetchWithCustomerRecovery(
|
||||
'https://api.test/customers/balance'
|
||||
)
|
||||
|
||||
expect(response.status).toBe(409)
|
||||
expect(countCustomerPosts()).toBe(1)
|
||||
})
|
||||
|
||||
it('should share one customer creation between concurrent credit pre-flights', async () => {
|
||||
mockFetch.mockImplementation((url: string, init?: RequestInit) => {
|
||||
if (url.endsWith('/customers') && init?.method === 'POST') {
|
||||
return Promise.resolve(mockCreateCustomerResponse)
|
||||
}
|
||||
if (url.endsWith('/customers/credit')) {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({ checkout_url: 'https://stripe.test/checkout' })
|
||||
})
|
||||
}
|
||||
return Promise.reject(new Error('Unexpected API call'))
|
||||
})
|
||||
|
||||
await Promise.all([
|
||||
store.initiateCreditPurchase({
|
||||
amount_micros: 5_000_000,
|
||||
currency: 'usd'
|
||||
}),
|
||||
store.initiateCreditPurchase({
|
||||
amount_micros: 5_000_000,
|
||||
currency: 'usd'
|
||||
})
|
||||
])
|
||||
|
||||
expect(countCustomerPosts()).toBe(1)
|
||||
})
|
||||
|
||||
it('stale rejection from previous session does not null out a new in-flight recovery', async () => {
|
||||
let rejectSession1Create!: (reason: unknown) => void
|
||||
let resolveSession2Create!: (value: Response) => void
|
||||
let postCount = 0
|
||||
|
||||
const session1CreateP = new Promise<Response>((_, reject) => {
|
||||
rejectSession1Create = reject
|
||||
})
|
||||
const session2CreateP = new Promise<Response>((resolve) => {
|
||||
resolveSession2Create = resolve
|
||||
})
|
||||
|
||||
mockFetch.mockImplementation((url: string, init?: RequestInit) => {
|
||||
if (url.endsWith('/customers') && init?.method === 'POST') {
|
||||
postCount++
|
||||
return postCount === 1 ? session1CreateP : session2CreateP
|
||||
}
|
||||
return Promise.resolve(makeConflictResponse())
|
||||
})
|
||||
|
||||
// Session 1: trigger a recovery whose POST will hang
|
||||
const session1Done = store
|
||||
.fetchWithCustomerRecovery('https://api.test/customers/balance')
|
||||
.then(() => 'session1')
|
||||
.catch(() => 'session1-failed')
|
||||
|
||||
// Drain microtasks so session1's POST is registered
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 0))
|
||||
|
||||
// Auth resets; new session starts
|
||||
authStateCallback(null)
|
||||
authStateCallback(mockUser)
|
||||
|
||||
// Session 2 recovery — should create a new independent in-flight promise
|
||||
const session2Done = store
|
||||
.fetchWithCustomerRecovery('https://api.test/customers/balance')
|
||||
.then(() => 'session2')
|
||||
.catch(() => 'session2-failed')
|
||||
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 0))
|
||||
|
||||
// Session 1's POST fails — stale rejection fires; must NOT null out session 2's recovery
|
||||
rejectSession1Create(new Error('network error'))
|
||||
await session1Done
|
||||
|
||||
// Session 2's POST resolves successfully
|
||||
resolveSession2Create({
|
||||
ok: true,
|
||||
statusText: 'OK',
|
||||
json: () => Promise.resolve({ id: 'id-2' })
|
||||
} as Response)
|
||||
const session2Result = await session2Done
|
||||
|
||||
// Session 2 must succeed, proving its recovery was not nulled by session 1's rejection
|
||||
expect(session2Result).toBe('session2')
|
||||
})
|
||||
|
||||
it('does not skip re-provisioning when createCustomer resolves after sign-out', async () => {
|
||||
let resolveCreate!: (value: Response) => void
|
||||
const slowCreateP = new Promise<Response>((resolve) => {
|
||||
resolveCreate = resolve
|
||||
})
|
||||
let postCount = 0
|
||||
|
||||
mockFetch.mockImplementation((url: string, init?: RequestInit) => {
|
||||
if (url.endsWith('/customers') && init?.method === 'POST') {
|
||||
postCount++
|
||||
return postCount === 1
|
||||
? slowCreateP
|
||||
: Promise.resolve(mockCreateCustomerResponse)
|
||||
}
|
||||
return Promise.resolve(makeConflictResponse())
|
||||
})
|
||||
|
||||
// Session 1 triggers recovery with a slow POST
|
||||
const session1Done = store
|
||||
.fetchWithCustomerRecovery('https://api.test/customers/balance')
|
||||
.catch(() => {})
|
||||
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 0))
|
||||
|
||||
// User signs out before the POST resolves
|
||||
authStateCallback(null)
|
||||
authStateCallback(mockUser)
|
||||
|
||||
// Stale POST resolves successfully after session reset
|
||||
resolveCreate!({
|
||||
ok: true,
|
||||
statusText: 'OK',
|
||||
json: () => Promise.resolve({ id: 'stale-id' })
|
||||
} as Response)
|
||||
await session1Done
|
||||
|
||||
// A fresh recovery for the new session must POST again;
|
||||
// customerCreated must not have been set by the stale resolution.
|
||||
await store.fetchWithCustomerRecovery(
|
||||
'https://api.test/customers/balance'
|
||||
)
|
||||
expect(postCount).toBe(2)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -69,6 +69,13 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
const currentUser = ref<User | null>(null)
|
||||
const isInitialized = ref(false)
|
||||
const customerCreated = ref(false)
|
||||
/**
|
||||
* Memoizes the in-flight or successful customer provisioning attempt for
|
||||
* the current account (see recoverMissingCustomer). Declared here so the
|
||||
* auth-state listener below can reset it before its initializer would
|
||||
* otherwise run.
|
||||
*/
|
||||
let customerRecovery: Promise<void> | null = null
|
||||
const isFetchingBalance = ref(false)
|
||||
|
||||
// Balance state
|
||||
@@ -134,6 +141,12 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
// Reset balance when auth state changes
|
||||
balance.value = null
|
||||
lastBalanceUpdateTime.value = null
|
||||
|
||||
// Customer provisioning state is per-account: without this reset, a
|
||||
// second account in the same browser session would be short-circuited by
|
||||
// the previous account's memoized recovery and stay stuck on 409s.
|
||||
customerCreated.value = false
|
||||
customerRecovery = null
|
||||
})
|
||||
|
||||
// Listen for token refresh events
|
||||
@@ -261,12 +274,15 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
throw new AuthStoreError(t('toastMessages.userNotAuthenticated'))
|
||||
}
|
||||
|
||||
const response = await fetch(buildApiUrl('/customers/balance'), {
|
||||
headers: {
|
||||
...authHeader,
|
||||
'Content-Type': 'application/json'
|
||||
const response = await fetchWithCustomerRecovery(
|
||||
buildApiUrl('/customers/balance'),
|
||||
{
|
||||
headers: {
|
||||
...authHeader,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
@@ -292,6 +308,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
}
|
||||
|
||||
const createCustomer = async (): Promise<CreateCustomerResponse> => {
|
||||
const sessionUserId = currentUser.value?.uid
|
||||
const authHeader = await getAuthHeader()
|
||||
if (!authHeader) {
|
||||
throw new AuthStoreError(t('toastMessages.userNotAuthenticated'))
|
||||
@@ -322,9 +339,116 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
)
|
||||
}
|
||||
|
||||
if (currentUser.value?.uid === sessionUserId) {
|
||||
customerCreated.value = true
|
||||
}
|
||||
return createCustomerResJson
|
||||
}
|
||||
|
||||
/**
|
||||
* Memoizes the customer provisioning attempt so concurrent or repeated 409
|
||||
* responses from /customers/* endpoints trigger at most one successful
|
||||
* POST /customers per account. A failed attempt is cleared so a later
|
||||
* request can retry, e.g. after a transient network failure.
|
||||
*/
|
||||
const recoverMissingCustomer = (): Promise<void> => {
|
||||
if (customerRecovery === null) {
|
||||
const thisRecovery: Promise<void> = createCustomer()
|
||||
.then(() => undefined)
|
||||
.catch((error: unknown) => {
|
||||
if (customerRecovery === thisRecovery) {
|
||||
customerRecovery = null
|
||||
}
|
||||
throw error
|
||||
})
|
||||
customerRecovery = thisRecovery
|
||||
}
|
||||
return customerRecovery
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch wrapper for /customers/* endpoints that self-heals accounts whose
|
||||
* customer record was never provisioned.
|
||||
*
|
||||
* Customer creation runs after the Firebase session is established during
|
||||
* sign-in/sign-up, so an interruption (navigation, closed window, network
|
||||
* failure) can leave a permanently signed-in user without a customer
|
||||
* record. Sessions restored from persisted credentials never re-run the
|
||||
* sign-in flow, so every /customers/* request fails with 409 and nothing
|
||||
* ever retries the creation.
|
||||
*
|
||||
* On a 409 response this provisions the customer record (deduplicated
|
||||
* across concurrent callers) and retries the original request a single
|
||||
* time. If recovery fails, the original 409 response is returned so
|
||||
* callers surface their normal error handling.
|
||||
*/
|
||||
/**
|
||||
* The auth middleware rejects requests for accounts without a customer
|
||||
* record using this exact message. Business-level 409s from /customers/*
|
||||
* endpoints (e.g. conflicting subscription state) must NOT trigger
|
||||
* provisioning or a blind retry of a payment request.
|
||||
*/
|
||||
const MISSING_CUSTOMER_MESSAGE = 'Failed to find customer'
|
||||
|
||||
const isMissingCustomerResponse = async (
|
||||
response: Response
|
||||
): Promise<boolean> => {
|
||||
if (response.status !== 409) return false
|
||||
try {
|
||||
const body: unknown = await response.clone().json()
|
||||
return (
|
||||
typeof body === 'object' &&
|
||||
body !== null &&
|
||||
'message' in body &&
|
||||
(body as { message: unknown }).message === MISSING_CUSTOMER_MESSAGE
|
||||
)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const isCustomerEndpoint = (input: string): boolean => {
|
||||
try {
|
||||
const { pathname } = new URL(input, window.location.href)
|
||||
return pathname === '/customers' || pathname.includes('/customers/')
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const fetchWithCustomerRecovery = async (
|
||||
input: string,
|
||||
init?: RequestInit
|
||||
): Promise<Response> => {
|
||||
const response = await fetch(input, init)
|
||||
if (
|
||||
!isCustomerEndpoint(input) ||
|
||||
!(await isMissingCustomerResponse(response))
|
||||
) {
|
||||
return response
|
||||
}
|
||||
|
||||
try {
|
||||
await recoverMissingCustomer()
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
'Customer provisioning during 409 recovery failed; returning original response',
|
||||
error
|
||||
)
|
||||
return response
|
||||
}
|
||||
|
||||
try {
|
||||
return await fetch(input, init)
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
'Retry after customer provisioning failed; returning original 409 response',
|
||||
error
|
||||
)
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
const executeAuthAction = async <T>(
|
||||
action: (auth: Auth) => Promise<T>,
|
||||
options: {
|
||||
@@ -467,20 +591,24 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
throw new AuthStoreError(t('toastMessages.userNotAuthenticated'))
|
||||
}
|
||||
|
||||
// Ensure customer was created during login/registration
|
||||
// Ensure customer was created during login/registration. Routed through
|
||||
// recoverMissingCustomer so a concurrent 409-triggered recovery and this
|
||||
// pre-flight share one POST /customers instead of racing.
|
||||
if (!customerCreated.value) {
|
||||
await createCustomer()
|
||||
customerCreated.value = true
|
||||
await recoverMissingCustomer()
|
||||
}
|
||||
|
||||
const response = await fetch(buildApiUrl('/customers/credit'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...authHeader,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(requestBodyContent)
|
||||
})
|
||||
const response = await fetchWithCustomerRecovery(
|
||||
buildApiUrl('/customers/credit'),
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...authHeader,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(requestBodyContent)
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
@@ -507,16 +635,19 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
throw new AuthStoreError(t('toastMessages.userNotAuthenticated'))
|
||||
}
|
||||
|
||||
const response = await fetch(buildApiUrl('/customers/billing'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...authHeader,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
...(targetTier && {
|
||||
body: JSON.stringify({ target_tier: targetTier })
|
||||
})
|
||||
})
|
||||
const response = await fetchWithCustomerRecovery(
|
||||
buildApiUrl('/customers/billing'),
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...authHeader,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
...(targetTier && {
|
||||
body: JSON.stringify({ target_tier: targetTier })
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
@@ -550,6 +681,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
register,
|
||||
logout,
|
||||
createCustomer,
|
||||
fetchWithCustomerRecovery,
|
||||
getIdToken,
|
||||
loginWithGoogle,
|
||||
loginWithGithub,
|
||||
|
||||
Reference in New Issue
Block a user