mirror of
https://github.com/modrinth/code.git
synced 2026-08-31 03:55:59 +00:00
hook up purchase server modal in app
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
// src/config.ts
|
||||||
|
export const config = {
|
||||||
|
siteUrl: import.meta.env.VITE_SITE_URL,
|
||||||
|
stripePublishableKey:
|
||||||
|
import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY ||
|
||||||
|
'pk_test_51JbFxJJygY5LJFfKV50mnXzz3YLvBVe2Gd1jn7ljWAkaBlRz3VQdxN9mXcPSrFbSqxwAb0svte9yhnsmm7qHfcWn00R611Ce7b',
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { Labrinth } from '@modrinth/api-client'
|
||||||
|
import { injectModrinthClient, ServersManagePageIndex } from '@modrinth/ui'
|
||||||
|
import { useQuery } from '@tanstack/vue-query'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { config } from '../config'
|
||||||
|
|
||||||
|
const stripePublishableKey = (config.stripePublishableKey as string) || ''
|
||||||
|
|
||||||
|
const client = injectModrinthClient()
|
||||||
|
|
||||||
|
const { data: products } = useQuery({
|
||||||
|
queryKey: ['billing', 'products'],
|
||||||
|
queryFn: () => client.labrinth.billing_internal.getProducts(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const resolvedProducts = computed<Labrinth.Billing.Internal.Product[]>(() => products.value ?? [])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ServersManagePageIndex
|
||||||
|
:stripe-publishable-key="stripePublishableKey"
|
||||||
|
:site-url="'https://modrinth.com'"
|
||||||
|
:products="resolvedProducts"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import Browse from './Browse.vue'
|
import Browse from './Browse.vue'
|
||||||
import Index from './Index.vue'
|
import Index from './Index.vue'
|
||||||
|
import Servers from './Servers.vue'
|
||||||
import Skins from './Skins.vue'
|
import Skins from './Skins.vue'
|
||||||
import Worlds from './Worlds.vue'
|
import Worlds from './Worlds.vue'
|
||||||
|
|
||||||
export { Browse, Index, Skins, Worlds }
|
export { Browse, Index, Servers, Skins, Worlds }
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { ServersManagePageIndex } from '@modrinth/ui'
|
|
||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
|
||||||
import * as Pages from '@/pages'
|
import * as Pages from '@/pages'
|
||||||
@@ -31,7 +30,7 @@ export default new createRouter({
|
|||||||
{
|
{
|
||||||
path: '/hosting/manage/',
|
path: '/hosting/manage/',
|
||||||
name: 'Servers',
|
name: 'Servers',
|
||||||
component: ServersManagePageIndex,
|
component: Pages.Servers,
|
||||||
meta: {
|
meta: {
|
||||||
breadcrumb: [{ name: 'Servers' }],
|
breadcrumb: [{ name: 'Servers' }],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
|
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
|
||||||
|
"types": ["vite/client"],
|
||||||
|
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,12 +87,12 @@
|
|||||||
"capabilities": ["ads", "core", "plugins"],
|
"capabilities": ["ads", "core", "plugins"],
|
||||||
"csp": {
|
"csp": {
|
||||||
"default-src": "'self' customprotocol: asset:",
|
"default-src": "'self' customprotocol: asset:",
|
||||||
"connect-src": "ipc: http://ipc.localhost https://modrinth.com https://*.modrinth.com https://*.posthog.com https://*.sentry.io https://api.mclo.gs http://textures.minecraft.net https://textures.minecraft.net 'self' data: blob:",
|
"connect-src": "ipc: http://ipc.localhost https://modrinth.com https://*.modrinth.com https://*.posthog.com https://posthog.modrinth.com https://*.sentry.io https://api.mclo.gs http://textures.minecraft.net https://textures.minecraft.net https://js.stripe.com https://*.stripe.com wss://*.stripe.com wss://*.nodes.modrinth.com 'self' data: blob:",
|
||||||
"font-src": ["https://cdn-raw.modrinth.com/fonts/"],
|
"font-src": ["https://cdn-raw.modrinth.com/fonts/"],
|
||||||
"img-src": "https: 'unsafe-inline' 'self' asset: http://asset.localhost http://textures.minecraft.net blob: data:",
|
"img-src": "https: 'unsafe-inline' 'self' asset: http://asset.localhost http://textures.minecraft.net blob: data:",
|
||||||
"style-src": "'unsafe-inline' 'self'",
|
"style-src": "'unsafe-inline' 'self'",
|
||||||
"script-src": "https://*.posthog.com https://tally.so/widgets/embed.js 'self'",
|
"script-src": "https://*.posthog.com https://posthog.modrinth.com https://js.stripe.com https://tally.so/widgets/embed.js 'self'",
|
||||||
"frame-src": "https://www.youtube.com https://www.youtube-nocookie.com https://discord.com https://tally.so/popup/ 'self'",
|
"frame-src": "https://www.youtube.com https://www.youtube-nocookie.com https://discord.com https://tally.so/popup/ https://js.stripe.com https://hooks.stripe.com 'self'",
|
||||||
"media-src": "https://*.githubusercontent.com"
|
"media-src": "https://*.githubusercontent.com"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -374,7 +374,7 @@ function goToBreadcrumbStep(id: string) {
|
|||||||
:ping="currentPing"
|
:ping="currentPing"
|
||||||
:loading="paymentMethodLoading"
|
:loading="paymentMethodLoading"
|
||||||
:selected-payment-method="selectedPaymentMethod || inputtedPaymentMethod"
|
:selected-payment-method="selectedPaymentMethod || inputtedPaymentMethod"
|
||||||
:has-payment-method="hasPaymentMethod"
|
:has-payment-method="!!hasPaymentMethod"
|
||||||
:tax="tax"
|
:tax="tax"
|
||||||
:total="total"
|
:total="total"
|
||||||
:no-payment-required="noPaymentRequired"
|
:no-payment-required="noPaymentRequired"
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ function setInterval(newInterval: ServerBillingInterval) {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="grid sm:grid-cols-[3fr_2fr] gap-4">
|
<div class="grid sm:grid-cols-[3fr_2fr] gap-4">
|
||||||
<div class="bg-table-alternateRow p-4 rounded-2xl">
|
<div class="bg-surface-2 p-4 rounded-2xl">
|
||||||
<div class="flex items-center gap-2 mb-3">
|
<div class="flex items-center gap-2 mb-3">
|
||||||
<ModrinthServersIcon class="flex h-5 w-fit" />
|
<ModrinthServersIcon class="flex h-5 w-fit" />
|
||||||
<TagItem>{{ planName }}</TagItem>
|
<TagItem>{{ planName }}</TagItem>
|
||||||
@@ -194,9 +194,7 @@ function setInterval(newInterval: ServerBillingInterval) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="bg-surface-2 p-4 rounded-2xl flex flex-col gap-2 items-center justify-center">
|
||||||
class="bg-table-alternateRow p-4 rounded-2xl flex flex-col gap-2 items-center justify-center"
|
|
||||||
>
|
|
||||||
<img
|
<img
|
||||||
v-if="flag"
|
v-if="flag"
|
||||||
class="aspect-[16/10] max-w-12 w-full object-cover rounded-md border-1 border-button-border border-solid"
|
class="aspect-[16/10] max-w-12 w-full object-cover rounded-md border-1 border-button-border border-solid"
|
||||||
@@ -304,10 +302,7 @@ function setInterval(newInterval: ServerBillingInterval) {
|
|||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<div
|
<div v-else class="p-4 rounded-2xl bg-surface-2 text-sm text-secondary leading-relaxed">
|
||||||
v-else
|
|
||||||
class="p-4 rounded-2xl bg-table-alternateRow text-sm text-secondary leading-relaxed"
|
|
||||||
>
|
|
||||||
No payment required. Your downgrade will apply at the end of the current billing period.
|
No payment required. Your downgrade will apply at the end of the current billing period.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,12 +3,26 @@
|
|||||||
data-pyro-server-list-root
|
data-pyro-server-list-root
|
||||||
class="experimental-styles-within relative mx-auto mb-6 flex min-h-screen w-full max-w-[1280px] flex-col px-6"
|
class="experimental-styles-within relative mx-auto mb-6 flex min-h-screen w-full max-w-[1280px] flex-col px-6"
|
||||||
>
|
>
|
||||||
<ServersUpgradeModalWrapper
|
<ModrinthServersPurchaseModal
|
||||||
v-if="isNuxt"
|
v-if="customer && regions"
|
||||||
ref="upgradeModal"
|
:key="`purchase-modal-${customer.id}`"
|
||||||
:stripe-publishable-key
|
ref="purchaseModal"
|
||||||
:site-url
|
:publishable-key="props.stripePublishableKey"
|
||||||
:products
|
:initiate-payment="
|
||||||
|
async (body) => await client.labrinth.billing_internal.initiatePayment(body)
|
||||||
|
"
|
||||||
|
:available-products="pyroProducts"
|
||||||
|
:on-error="handleError"
|
||||||
|
:customer="customer"
|
||||||
|
:payment-methods="paymentMethods ?? []"
|
||||||
|
:currency="selectedCurrency"
|
||||||
|
:return-url="`${props.siteUrl}/hosting/manage`"
|
||||||
|
:pings="regionPings"
|
||||||
|
:regions="regions"
|
||||||
|
:refresh-payment-methods="fetchPaymentData"
|
||||||
|
:fetch-stock="fetchStock"
|
||||||
|
:affiliate-code="affiliateCode"
|
||||||
|
plan-stage
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -98,7 +112,10 @@
|
|||||||
<h1 class="m-0 text-contrast">You don't have any servers yet!</h1>
|
<h1 class="m-0 text-contrast">You don't have any servers yet!</h1>
|
||||||
<p class="m-0">Modrinth Hosting is a new way to play modded Minecraft with your friends.</p>
|
<p class="m-0">Modrinth Hosting is a new way to play modded Minecraft with your friends.</p>
|
||||||
<ButtonStyled size="large" type="standard" color="brand">
|
<ButtonStyled size="large" type="standard" color="brand">
|
||||||
<AutoLink to="/servers#plan">Create a server</AutoLink>
|
<AutoLink v-if="isNuxt" to="/servers#plan">Create a server</AutoLink>
|
||||||
|
<button v-else :disabled="!canOpenPurchaseModal" @click="openPurchaseModal">
|
||||||
|
Create a server
|
||||||
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -116,11 +133,15 @@
|
|||||||
placeholder="Search servers..."
|
placeholder="Search servers..."
|
||||||
wrapper-class="w-full md:w-72"
|
wrapper-class="w-full md:w-72"
|
||||||
/>
|
/>
|
||||||
<ButtonStyled v-if="isNuxt" type="standard">
|
<ButtonStyled type="standard">
|
||||||
<AutoLink :to="{ path: '/servers', hash: '#plan' }">
|
<AutoLink v-if="isNuxt" :to="{ path: '/servers', hash: '#plan' }">
|
||||||
<PlusIcon />
|
<PlusIcon />
|
||||||
New server
|
New server
|
||||||
</AutoLink>
|
</AutoLink>
|
||||||
|
<button v-else :disabled="!canOpenPurchaseModal" @click="openPurchaseModal">
|
||||||
|
<PlusIcon />
|
||||||
|
New server
|
||||||
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -152,7 +173,7 @@
|
|||||||
v-for="server in filteredData.filter((s) => s.is_medal)"
|
v-for="server in filteredData.filter((s) => s.is_medal)"
|
||||||
:key="server.server_id"
|
:key="server.server_id"
|
||||||
v-bind="server"
|
v-bind="server"
|
||||||
@upgrade="openUpgradeModal(server.server_id)"
|
@upgrade="openPurchaseModal"
|
||||||
/>
|
/>
|
||||||
<ServerListing
|
<ServerListing
|
||||||
v-for="server in filteredData.filter((s) => !s.is_medal)"
|
v-for="server in filteredData.filter((s) => !s.is_medal)"
|
||||||
@@ -180,24 +201,24 @@ import {
|
|||||||
CopyCode,
|
CopyCode,
|
||||||
injectModrinthClient,
|
injectModrinthClient,
|
||||||
injectNotificationManager,
|
injectNotificationManager,
|
||||||
|
ModrinthServersPurchaseModal,
|
||||||
StyledInput,
|
StyledInput,
|
||||||
} from '@modrinth/ui'
|
} from '@modrinth/ui'
|
||||||
import type { ModrinthServersFetchError } from '@modrinth/utils'
|
import type { ModrinthServersFetchError } from '@modrinth/utils'
|
||||||
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import Fuse from 'fuse.js'
|
import Fuse from 'fuse.js'
|
||||||
import type { ComponentPublicInstance } from 'vue'
|
import type Stripe from 'stripe'
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
|
||||||
import ServersUpgradeModalWrapper from '#ui/components/billing/ServersUpgradeModalWrapper.vue'
|
|
||||||
import MedalServerListing from '#ui/components/servers/marketing/MedalServerListing.vue'
|
import MedalServerListing from '#ui/components/servers/marketing/MedalServerListing.vue'
|
||||||
import ServerListing from '#ui/components/servers/ServerListing.vue'
|
import ServerListing from '#ui/components/servers/ServerListing.vue'
|
||||||
|
|
||||||
defineProps<{
|
const props = defineProps<{
|
||||||
stripePublishableKey?: string
|
stripePublishableKey: string
|
||||||
siteUrl?: string
|
siteUrl: string
|
||||||
products?: Labrinth.Billing.Internal.Product[]
|
products: Labrinth.Billing.Internal.Product[]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -214,6 +235,155 @@ const pollingState = ref({
|
|||||||
initialServers: [] as Archon.Servers.v0.Server[],
|
initialServers: [] as Archon.Servers.v0.Server[],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const purchaseModal = ref<InstanceType<typeof ModrinthServersPurchaseModal> | null>(null)
|
||||||
|
const affiliateCode = ref<string | null>(null)
|
||||||
|
const selectedCurrency = ref<string>('USD')
|
||||||
|
const regionPings = ref<
|
||||||
|
{
|
||||||
|
region: string
|
||||||
|
ping: number
|
||||||
|
}[]
|
||||||
|
>([])
|
||||||
|
|
||||||
|
const pyroProducts = computed(() => {
|
||||||
|
return [...props.products]
|
||||||
|
.filter((p) => p?.metadata?.type === 'pyro' || p?.metadata?.type === 'medal')
|
||||||
|
.sort((a, b) => {
|
||||||
|
const aRam =
|
||||||
|
a?.metadata?.type === 'pyro' || a?.metadata?.type === 'medal' ? a.metadata.ram : 0
|
||||||
|
const bRam =
|
||||||
|
b?.metadata?.type === 'pyro' || b?.metadata?.type === 'medal' ? b.metadata.ram : 0
|
||||||
|
return aRam - bRam
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: customer,
|
||||||
|
refetch: refetchCustomer,
|
||||||
|
isLoading: customerLoading,
|
||||||
|
} = useQuery({
|
||||||
|
queryKey: ['billing', 'customer'],
|
||||||
|
queryFn: () => client.labrinth.billing_internal.getCustomer() as Promise<Stripe.Customer>,
|
||||||
|
})
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: paymentMethods,
|
||||||
|
refetch: refetchPaymentMethods,
|
||||||
|
isLoading: paymentMethodsLoading,
|
||||||
|
} = useQuery({
|
||||||
|
queryKey: ['billing', 'payment-methods'],
|
||||||
|
queryFn: () =>
|
||||||
|
client.labrinth.billing_internal.getPaymentMethods() as Promise<Stripe.PaymentMethod[]>,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { data: regions, isLoading: regionsLoading } = useQuery({
|
||||||
|
queryKey: ['servers', 'regions'],
|
||||||
|
queryFn: () => client.archon.servers_v1.getRegions(),
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
regions,
|
||||||
|
(newRegions) => {
|
||||||
|
regionPings.value = []
|
||||||
|
if (newRegions) {
|
||||||
|
newRegions.forEach((region) => {
|
||||||
|
runPingTest(region)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
async function fetchPaymentData() {
|
||||||
|
await Promise.all([refetchCustomer(), refetchPaymentMethods()])
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchStock(
|
||||||
|
region: Archon.Servers.v1.Region,
|
||||||
|
request: Archon.Servers.v0.StockRequest,
|
||||||
|
): Promise<number> {
|
||||||
|
const result = await client.archon.servers_v0.checkStock(region.shortcode, request)
|
||||||
|
return result.available
|
||||||
|
}
|
||||||
|
|
||||||
|
const PING_COUNT = 20
|
||||||
|
const PING_INTERVAL = 200
|
||||||
|
const MAX_PING_TIME = 1000
|
||||||
|
|
||||||
|
function runPingTest(region: Archon.Servers.v1.Region, index = 1) {
|
||||||
|
if (index > 10) {
|
||||||
|
regionPings.value = regionPings.value.filter((entry) => entry.region !== region.shortcode)
|
||||||
|
regionPings.value.push({
|
||||||
|
region: region.shortcode,
|
||||||
|
ping: -1,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const wsUrl = `wss://${region.shortcode}${index}.${region.zone}/pingtest`
|
||||||
|
try {
|
||||||
|
const socket = new WebSocket(wsUrl)
|
||||||
|
const pings: number[] = []
|
||||||
|
let finalized = false
|
||||||
|
|
||||||
|
const finalize = (ping: number) => {
|
||||||
|
if (finalized) return
|
||||||
|
finalized = true
|
||||||
|
clearTimeout(connectTimeout)
|
||||||
|
regionPings.value = regionPings.value.filter((entry) => entry.region !== region.shortcode)
|
||||||
|
regionPings.value.push({
|
||||||
|
region: region.shortcode,
|
||||||
|
ping,
|
||||||
|
})
|
||||||
|
socket.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
const retryNext = () => {
|
||||||
|
if (finalized) return
|
||||||
|
finalized = true
|
||||||
|
clearTimeout(connectTimeout)
|
||||||
|
socket.close()
|
||||||
|
runPingTest(region, index + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent hangs where the socket never opens or errors.
|
||||||
|
const connectTimeout = setTimeout(() => {
|
||||||
|
retryNext()
|
||||||
|
}, 3000)
|
||||||
|
|
||||||
|
socket.onopen = () => {
|
||||||
|
clearTimeout(connectTimeout)
|
||||||
|
|
||||||
|
for (let i = 0; i < PING_COUNT; i++) {
|
||||||
|
setTimeout(() => {
|
||||||
|
socket.send(String(performance.now()))
|
||||||
|
}, i * PING_INTERVAL)
|
||||||
|
}
|
||||||
|
setTimeout(
|
||||||
|
() => {
|
||||||
|
const median =
|
||||||
|
pings.length > 0
|
||||||
|
? Math.round([...pings].sort((a, b) => a - b)[Math.floor(pings.length / 2)])
|
||||||
|
: -1
|
||||||
|
finalize(median)
|
||||||
|
},
|
||||||
|
PING_COUNT * PING_INTERVAL + MAX_PING_TIME,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.onmessage = (event) => {
|
||||||
|
const start = Number(event.data)
|
||||||
|
pings.push(performance.now() - start)
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.onerror = () => {
|
||||||
|
retryNext()
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
runPingTest(region, index + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: serverResponse,
|
data: serverResponse,
|
||||||
error: fetchError,
|
error: fetchError,
|
||||||
@@ -309,18 +479,43 @@ watch(serverResponse, (response) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
type ServersUpgradeModalWrapperRef = ComponentPublicInstance<{
|
|
||||||
open: (id: string) => void | Promise<void>
|
|
||||||
}>
|
|
||||||
|
|
||||||
const upgradeModal = ref<ServersUpgradeModalWrapperRef | null>(null)
|
|
||||||
function openUpgradeModal(serverId: string) {
|
|
||||||
upgradeModal.value?.open(serverId)
|
|
||||||
}
|
|
||||||
|
|
||||||
const { addNotification } = injectNotificationManager()
|
const { addNotification } = injectNotificationManager()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
const canOpenPurchaseModal = computed(() => {
|
||||||
|
return (
|
||||||
|
Boolean(props.stripePublishableKey) &&
|
||||||
|
Boolean(customer.value) &&
|
||||||
|
paymentMethods.value !== undefined &&
|
||||||
|
Boolean(regions.value) &&
|
||||||
|
!customerLoading.value &&
|
||||||
|
!paymentMethodsLoading.value &&
|
||||||
|
!regionsLoading.value
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleError(err: unknown) {
|
||||||
|
const error = err as Error & { data?: { description?: string } }
|
||||||
|
addNotification({
|
||||||
|
title: 'An error occurred',
|
||||||
|
type: 'error',
|
||||||
|
text: error?.message ?? error?.data?.description ?? String(err),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function openPurchaseModal() {
|
||||||
|
if (!canOpenPurchaseModal.value || !purchaseModal.value) {
|
||||||
|
addNotification({
|
||||||
|
title: 'Purchase unavailable',
|
||||||
|
text: 'Payment information is still loading. Please try again in a moment.',
|
||||||
|
type: 'warning',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
purchaseModal.value.show('quarterly')
|
||||||
|
}
|
||||||
|
|
||||||
const { data: subscriptions } = useQuery({
|
const { data: subscriptions } = useQuery({
|
||||||
queryKey: ['billing', 'subscriptions'],
|
queryKey: ['billing', 'subscriptions'],
|
||||||
queryFn: () => client.labrinth.billing_internal.getSubscriptions(),
|
queryFn: () => client.labrinth.billing_internal.getSubscriptions(),
|
||||||
|
|||||||
Reference in New Issue
Block a user