You've already forked AstralRinth
forked from didirus/AstralRinth
Add quick server button, dynamic price preview for custom server modal (#3815)
* Add quick server creation button, and dynamic pricing to custom server selection * Remove test in compatibility card * Lint + remove duplicate file * Adjust z-index of popup * $6 -> $5 * Dismiss prompt if the button is clicked * Make "Create a server" disabled for now * Use existing loaders type
This commit is contained in:
@@ -59,6 +59,7 @@ const selectedPlan = ref<ServerPlan>()
|
||||
const selectedInterval = ref<ServerBillingInterval>('quarterly')
|
||||
const loading = ref(false)
|
||||
const selectedRegion = ref<string>()
|
||||
const projectId = ref<string>()
|
||||
|
||||
const {
|
||||
initializeStripe,
|
||||
@@ -85,6 +86,7 @@ const {
|
||||
selectedPlan,
|
||||
selectedInterval,
|
||||
selectedRegion,
|
||||
projectId,
|
||||
props.initiatePayment,
|
||||
props.onError,
|
||||
)
|
||||
@@ -201,7 +203,7 @@ watch(selectedPlan, () => {
|
||||
console.log(selectedPlan.value)
|
||||
})
|
||||
|
||||
function begin(interval: ServerBillingInterval, plan?: ServerPlan) {
|
||||
function begin(interval: ServerBillingInterval, plan?: ServerPlan, project?: string) {
|
||||
loading.value = false
|
||||
selectedPlan.value = plan
|
||||
selectedInterval.value = interval
|
||||
@@ -209,6 +211,7 @@ function begin(interval: ServerBillingInterval, plan?: ServerPlan) {
|
||||
selectedPaymentMethod.value = undefined
|
||||
currentStep.value = steps[0]
|
||||
skipPaymentMethods.value = true
|
||||
projectId.value = project
|
||||
modal.value?.show()
|
||||
}
|
||||
|
||||
@@ -253,6 +256,8 @@ defineExpose({
|
||||
:pings="pings"
|
||||
:custom="customServer"
|
||||
:available-products="availableProducts"
|
||||
:currency="currency"
|
||||
:interval="selectedInterval"
|
||||
:fetch-stock="fetchStock"
|
||||
/>
|
||||
<PaymentMethodSelector
|
||||
|
||||
@@ -4,19 +4,28 @@ import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||
import { IntlFormatted } from '@vintl/vintl/components'
|
||||
import { onMounted, ref, computed, watch } from 'vue'
|
||||
import type { RegionPing } from './ModrinthServersPurchaseModal.vue'
|
||||
import type { ServerPlan, ServerRegion, ServerStockRequest } from '../../utils/billing'
|
||||
import {
|
||||
monthsInInterval,
|
||||
type ServerBillingInterval,
|
||||
type ServerPlan,
|
||||
type ServerRegion,
|
||||
type ServerStockRequest,
|
||||
} from '../../utils/billing'
|
||||
import ModalLoadingIndicator from '../modal/ModalLoadingIndicator.vue'
|
||||
import Slider from '../base/Slider.vue'
|
||||
import { SpinnerIcon, XIcon, InfoIcon } from '@modrinth/assets'
|
||||
import ServersSpecs from './ServersSpecs.vue'
|
||||
import { formatPrice } from '../../../../utils'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const { formatMessage, locale } = useVIntl()
|
||||
|
||||
const props = defineProps<{
|
||||
regions: ServerRegion[]
|
||||
pings: RegionPing[]
|
||||
fetchStock: (region: ServerRegion, request: ServerStockRequest) => Promise<number>
|
||||
custom: boolean
|
||||
currency: string
|
||||
interval: ServerBillingInterval
|
||||
availableProducts: ServerPlan[]
|
||||
}>()
|
||||
|
||||
@@ -25,6 +34,12 @@ const checkingCustomStock = ref(false)
|
||||
const selectedPlan = defineModel<ServerPlan>('plan')
|
||||
const selectedRegion = defineModel<string>('region')
|
||||
|
||||
const selectedPrice = computed(() => {
|
||||
const amount = selectedPlan.value?.prices?.find((price) => price.currency_code === props.currency)
|
||||
?.prices?.intervals?.[props.interval]
|
||||
return amount ? amount / monthsInInterval[props.interval] : undefined
|
||||
})
|
||||
|
||||
const regionOrder: string[] = ['us-vin', 'eu-lim']
|
||||
|
||||
const sortedRegions = computed(() => {
|
||||
@@ -216,7 +231,12 @@ onMounted(() => {
|
||||
</h2>
|
||||
<div>
|
||||
<Slider v-model="selectedRam" :min="minRam" :max="maxRam" :step="2" unit="GB" />
|
||||
<div class="bg-bg rounded-xl p-4 mt-4 text-secondary">
|
||||
<p v-if="selectedPrice" class="mt-2 mb-0">
|
||||
<span class="text-contrast text-lg font-bold"
|
||||
>{{ formatPrice(locale, selectedPrice, currency, true) }} / month</span
|
||||
><span v-if="interval !== 'monthly'">, billed {{ interval }}</span>
|
||||
</p>
|
||||
<div class="bg-bg rounded-xl p-4 mt-2 text-secondary">
|
||||
<div v-if="checkingCustomStock" class="flex gap-2 items-center">
|
||||
<SpinnerIcon class="size-5 shrink-0 animate-spin" /> Checking availability...
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { ServerBillingInterval, ServerPlan, ServerRegion } from '../../utils/billing'
|
||||
import {
|
||||
monthsInInterval,
|
||||
type ServerBillingInterval,
|
||||
type ServerPlan,
|
||||
type ServerRegion,
|
||||
} from '../../utils/billing'
|
||||
import TagItem from '../base/TagItem.vue'
|
||||
import ServersSpecs from './ServersSpecs.vue'
|
||||
import { formatPrice, getPingLevel } from '@modrinth/utils'
|
||||
@@ -77,12 +82,6 @@ const period = computed(() => {
|
||||
return '???'
|
||||
})
|
||||
|
||||
const monthsInInterval: Record<ServerBillingInterval, number> = {
|
||||
monthly: 1,
|
||||
quarterly: 3,
|
||||
yearly: 12,
|
||||
}
|
||||
|
||||
function setInterval(newInterval: ServerBillingInterval) {
|
||||
interval.value = newInterval
|
||||
emit('reloadPaymentIntent')
|
||||
|
||||
@@ -109,5 +109,6 @@ export { default as VersionSummary } from './version/VersionSummary.vue'
|
||||
export { default as ThemeSelector } from './settings/ThemeSelector.vue'
|
||||
|
||||
// Servers
|
||||
export { default as ServersPromo } from './servers/ServersPromo.vue'
|
||||
export { default as BackupWarning } from './servers/backups/BackupWarning.vue'
|
||||
export { default as ServersSpecs } from './billing/ServersSpecs.vue'
|
||||
|
||||
60
packages/ui/src/components/servers/ServersPromo.vue
Normal file
60
packages/ui/src/components/servers/ServersPromo.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<script setup lang="ts">
|
||||
import { RightArrowIcon, ModrinthIcon, XIcon } from '@modrinth/assets'
|
||||
import ButtonStyled from '../base/ButtonStyled.vue'
|
||||
import AutoLink from '../base/AutoLink.vue'
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'close'): void
|
||||
}>()
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
link: string
|
||||
closable?: boolean
|
||||
}>(),
|
||||
{
|
||||
closable: true,
|
||||
},
|
||||
)
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
class="brand-gradient-bg card-shadow bg-bg relative p-4 border-[1px] border-solid border-brand rounded-2xl grid grid-cols-[1fr_auto] overflow-hidden"
|
||||
>
|
||||
<ModrinthIcon
|
||||
class="absolute -top-12 -right-12 size-48 text-brand-highlight opacity-25"
|
||||
fill="none"
|
||||
stroke="var(--color-brand)"
|
||||
stroke-width="4"
|
||||
/>
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="text-lg leading-tight font-extrabold text-contrast"
|
||||
>Want to play with <br />
|
||||
<span class="text-brand">your friends?</span></span
|
||||
>
|
||||
<span class="text-sm font-medium">Create a server with Modrinth in just a few clicks.</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-end justify-end z-10">
|
||||
<ButtonStyled color="brand">
|
||||
<AutoLink :to="link"> View plans <RightArrowIcon /> </AutoLink>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div class="absolute top-2 right-2 z-10">
|
||||
<ButtonStyled v-if="closable" size="small" circular>
|
||||
<button v-tooltip="`Don't show again`" @click="emit('close')">
|
||||
<XIcon aria-hidden="true" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped>
|
||||
.brand-gradient-bg {
|
||||
background-image: linear-gradient(
|
||||
to top right,
|
||||
var(--color-brand-highlight) -80%,
|
||||
var(--color-bg)
|
||||
);
|
||||
--color-button-bg: var(--brand-gradient-button);
|
||||
}
|
||||
</style>
|
||||
@@ -31,6 +31,7 @@ export const useStripe = (
|
||||
product: Ref<ServerPlan | undefined>,
|
||||
interval: Ref<ServerBillingInterval>,
|
||||
region: Ref<string | undefined>,
|
||||
project: Ref<string | undefined>,
|
||||
initiatePayment: (
|
||||
body: CreatePaymentIntentRequest | UpdatePaymentIntentRequest,
|
||||
) => Promise<CreatePaymentIntentResponse | UpdatePaymentIntentResponse>,
|
||||
@@ -222,16 +223,22 @@ export const useStripe = (
|
||||
|
||||
let result: BasePaymentIntentResponse
|
||||
|
||||
const metadata: CreatePaymentIntentRequest['metadata'] = {
|
||||
type: 'pyro',
|
||||
server_region: region.value,
|
||||
source: project.value
|
||||
? {
|
||||
project_id: project.value,
|
||||
}
|
||||
: {},
|
||||
}
|
||||
|
||||
if (paymentIntentId.value) {
|
||||
result = await updateIntent({
|
||||
...requestType,
|
||||
charge,
|
||||
existing_payment_intent: paymentIntentId.value,
|
||||
metadata: {
|
||||
type: 'pyro',
|
||||
server_region: region.value,
|
||||
source: {},
|
||||
},
|
||||
metadata,
|
||||
})
|
||||
console.log(`Updated payment intent: ${interval.value} for ${result.total}`)
|
||||
} else {
|
||||
@@ -242,11 +249,7 @@ export const useStripe = (
|
||||
} = await createIntent({
|
||||
...requestType,
|
||||
charge,
|
||||
metadata: {
|
||||
type: 'pyro',
|
||||
server_region: region.value,
|
||||
source: {},
|
||||
},
|
||||
metadata: metadata,
|
||||
}))
|
||||
console.log(`Created payment intent: ${interval.value} for ${result.total}`)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import type Stripe from 'stripe'
|
||||
import type { Loaders } from '@modrinth/utils'
|
||||
|
||||
export type ServerBillingInterval = 'monthly' | 'yearly' | 'quarterly'
|
||||
|
||||
export const monthsInInterval: Record<ServerBillingInterval, number> = {
|
||||
monthly: 1,
|
||||
quarterly: 3,
|
||||
yearly: 12,
|
||||
}
|
||||
|
||||
export interface ServerPlan {
|
||||
id: string
|
||||
name: string
|
||||
@@ -72,11 +79,18 @@ export type CreatePaymentIntentRequest = PaymentRequestType & {
|
||||
type: 'pyro'
|
||||
server_name?: string
|
||||
server_region?: string
|
||||
source: {
|
||||
loader?: string
|
||||
game_version?: string
|
||||
loader_version?: string
|
||||
}
|
||||
source:
|
||||
| {
|
||||
loader: Loaders
|
||||
game_version?: string
|
||||
loader_version?: string
|
||||
}
|
||||
| {
|
||||
project_id: string
|
||||
version_id?: string
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
| {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user