fix: upgrade server flow to skip region (#5842)
* fix: upgrade server flow to skip region * remove: previous hide region select implementation * feat: implement skipping region select section for upgrade modal * fix: modal not getting stripe customer and payment methods on page hard refresh * refactor: pnpm prepr
This commit is contained in:
@@ -10,7 +10,7 @@ import {
|
||||
} from '@modrinth/assets'
|
||||
import { useQueryClient } from '@tanstack/vue-query'
|
||||
import type Stripe from 'stripe'
|
||||
import { computed, nextTick, ref, useTemplateRef, watch } from 'vue'
|
||||
import { computed, nextTick, ref, toRef, useTemplateRef, watch } from 'vue'
|
||||
|
||||
import { injectNotificationManager } from '#ui/providers/web-notifications.ts'
|
||||
|
||||
@@ -94,8 +94,8 @@ const {
|
||||
noPaymentRequired,
|
||||
} = useStripe(
|
||||
props.publishableKey,
|
||||
props.customer,
|
||||
props.paymentMethods,
|
||||
toRef(props, 'customer'),
|
||||
toRef(props, 'paymentMethods'),
|
||||
props.currency,
|
||||
selectedPlan,
|
||||
selectedInterval,
|
||||
@@ -116,6 +116,10 @@ const steps: Step[] = props.planStage
|
||||
? (['plan', 'region', 'payment', 'review'] as Step[])
|
||||
: (['region', 'payment', 'review'] as Step[])
|
||||
|
||||
const isUpgrade = computed(() => !!props.existingSubscription)
|
||||
const skipRegionStep = computed(() => isUpgrade.value && !customServer.value)
|
||||
const visibleSteps = computed(() => steps.filter((s) => !(s === 'region' && skipRegionStep.value)))
|
||||
|
||||
const titles: Record<Step, MessageDescriptor> = {
|
||||
plan: defineMessage({ id: 'servers.purchase.step.plan.title', defaultMessage: 'Plan' }),
|
||||
region: defineMessage({ id: 'servers.purchase.step.region.title', defaultMessage: 'Region' }),
|
||||
@@ -146,17 +150,24 @@ const currentPing = computed(() => {
|
||||
|
||||
const currentStep = ref<Step>()
|
||||
|
||||
const currentStepIndex = computed(() => (currentStep.value ? steps.indexOf(currentStep.value) : -1))
|
||||
const currentStepIndex = computed(() =>
|
||||
currentStep.value ? visibleSteps.value.indexOf(currentStep.value) : -1,
|
||||
)
|
||||
const previousStep = computed(() => {
|
||||
const step = currentStep.value ? steps[steps.indexOf(currentStep.value) - 1] : undefined
|
||||
if (!currentStep.value) return undefined
|
||||
const idx = visibleSteps.value.indexOf(currentStep.value)
|
||||
let step = idx > 0 ? visibleSteps.value[idx - 1] : undefined
|
||||
if (step === 'payment' && skipPaymentMethods.value && primaryPaymentMethodId.value) {
|
||||
return 'region'
|
||||
const paymentIdx = visibleSteps.value.indexOf('payment')
|
||||
step = paymentIdx > 0 ? visibleSteps.value[paymentIdx - 1] : undefined
|
||||
}
|
||||
return step
|
||||
})
|
||||
const nextStep = computed(() =>
|
||||
currentStep.value ? steps[steps.indexOf(currentStep.value) + 1] : undefined,
|
||||
)
|
||||
const nextStep = computed(() => {
|
||||
if (!currentStep.value) return undefined
|
||||
const idx = visibleSteps.value.indexOf(currentStep.value)
|
||||
return idx >= 0 ? visibleSteps.value[idx + 1] : undefined
|
||||
})
|
||||
|
||||
const canProceed = computed(() => {
|
||||
switch (currentStep.value) {
|
||||
@@ -195,12 +206,14 @@ async function beforeProceed(step: string) {
|
||||
await initializeStripe()
|
||||
|
||||
if (primaryPaymentMethodId.value && skipPaymentMethods.value) {
|
||||
const paymentMethod = await props.paymentMethods.find(
|
||||
const paymentMethod = props.paymentMethods.find(
|
||||
(x) => x.id === primaryPaymentMethodId.value,
|
||||
)
|
||||
await selectPaymentMethod(paymentMethod)
|
||||
await setStep('review', true)
|
||||
return false
|
||||
if (paymentMethod) {
|
||||
await selectPaymentMethod(paymentMethod)
|
||||
await setStep('review', true)
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
case 'review':
|
||||
@@ -261,11 +274,25 @@ async function setStep(step: Step | undefined, skipValidation = false) {
|
||||
}
|
||||
}
|
||||
|
||||
watch(selectedPlan, () => {
|
||||
if (currentStep.value === 'plan') {
|
||||
customServer.value = !selectedPlan.value
|
||||
}
|
||||
})
|
||||
watch(
|
||||
selectedPlan,
|
||||
() => {
|
||||
if (currentStep.value === 'plan') {
|
||||
customServer.value = !selectedPlan.value
|
||||
}
|
||||
},
|
||||
{ flush: 'sync' },
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.existingSubscription,
|
||||
(sub) => {
|
||||
if (sub?.metadata?.type === 'pyro' && sub.metadata.region) {
|
||||
selectedRegion.value = sub.metadata.region
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
const defaultPlan = computed<Labrinth.Billing.Internal.Product | undefined>(() => {
|
||||
return (
|
||||
@@ -295,7 +322,9 @@ function begin(
|
||||
customServer.value = !selectedPlan.value
|
||||
selectedPaymentMethod.value = undefined
|
||||
const skipPlanStep = props.planStage && plan !== undefined
|
||||
currentStep.value = skipPlanStep ? (steps[1] ?? steps[0]) : steps[0]
|
||||
currentStep.value = skipPlanStep
|
||||
? (visibleSteps.value[1] ?? visibleSteps.value[0])
|
||||
: visibleSteps.value[0]
|
||||
skipPaymentMethods.value = true
|
||||
projectId.value = project
|
||||
modal.value?.show()
|
||||
@@ -339,7 +368,7 @@ function goToBreadcrumbStep(id: string) {
|
||||
<NewModal ref="modal" @hide="$emit('hide')">
|
||||
<template #title>
|
||||
<div class="flex items-center gap-1 font-bold text-secondary">
|
||||
<template v-for="(step, index) in steps" :key="step">
|
||||
<template v-for="(step, index) in visibleSteps" :key="step">
|
||||
<button
|
||||
v-if="index < currentStepIndex"
|
||||
class="bg-transparent active:scale-95 font-bold text-secondary p-0"
|
||||
@@ -356,7 +385,7 @@ function goToBreadcrumbStep(id: string) {
|
||||
{{ formatMessage(titles[step]) }}
|
||||
</span>
|
||||
<ChevronRightIcon
|
||||
v-if="index < steps.length - 1"
|
||||
v-if="index < visibleSteps.length - 1"
|
||||
class="h-5 w-5 text-secondary"
|
||||
stroke-width="3"
|
||||
/>
|
||||
@@ -381,6 +410,7 @@ function goToBreadcrumbStep(id: string) {
|
||||
:regions="regions"
|
||||
:pings="pings"
|
||||
:custom="customServer"
|
||||
:hide-region-selection="isUpgrade"
|
||||
:available-products="availableProducts"
|
||||
:currency="currency"
|
||||
:interval="selectedInterval"
|
||||
|
||||
@@ -25,6 +25,7 @@ const props = defineProps<{
|
||||
request: Archon.Servers.v0.StockRequest,
|
||||
) => Promise<number>
|
||||
custom: boolean
|
||||
hideRegionSelection?: boolean
|
||||
currency: string
|
||||
interval: ServerBillingInterval
|
||||
availableProducts: Labrinth.Billing.Internal.Product[]
|
||||
@@ -177,6 +178,10 @@ const messages = defineMessages({
|
||||
id: 'servers.region.custom.prompt',
|
||||
defaultMessage: `How much RAM do you want your server to have?`,
|
||||
},
|
||||
customPromptRamOnly: {
|
||||
id: 'servers.region.custom.prompt-ram-only',
|
||||
defaultMessage: `RAM`,
|
||||
},
|
||||
})
|
||||
|
||||
async function updateStock() {
|
||||
@@ -219,8 +224,10 @@ onMounted(() => {
|
||||
if (b.ping <= 0) return -1
|
||||
return a.ping - b.ping
|
||||
})[0]?.region
|
||||
selectedRegion.value = undefined
|
||||
selectedRam.value = minRam.value
|
||||
if (!props.hideRegionSelection) {
|
||||
selectedRegion.value = undefined
|
||||
}
|
||||
checkingCustomStock.value = true
|
||||
updateStock().then(() => {
|
||||
const firstWithStock = sortedRegions.value.find(
|
||||
@@ -228,8 +235,9 @@ onMounted(() => {
|
||||
)
|
||||
let stockedRegion = selectedRegion.value
|
||||
if (!stockedRegion) {
|
||||
stockedRegion =
|
||||
bestPing.value && currentStock.value[bestPing.value] > 0
|
||||
stockedRegion = props.hideRegionSelection
|
||||
? firstWithStock?.shortcode
|
||||
: bestPing.value && currentStock.value[bestPing.value] > 0
|
||||
? bestPing.value
|
||||
: firstWithStock?.shortcode
|
||||
}
|
||||
@@ -247,37 +255,44 @@ onMounted(() => {
|
||||
Checking availability...
|
||||
</ModalLoadingIndicator>
|
||||
<template v-else>
|
||||
<h2 class="mt-0 mb-4 text-xl font-bold text-contrast">
|
||||
{{ formatMessage(messages.prompt) }}
|
||||
</h2>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<ServersRegionButton
|
||||
v-for="region in visibleRegions"
|
||||
:key="region.shortcode"
|
||||
v-model="selectedRegion"
|
||||
:region="region"
|
||||
:out-of-stock="currentStock[region.shortcode] === 0"
|
||||
:ping="pings.find((p) => p.region === region.shortcode)?.ping"
|
||||
:best-ping="bestPing === region.shortcode"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-3 text-sm">
|
||||
<IntlFormatted :message-id="messages.regionUnsupported">
|
||||
<template #link="{ children }">
|
||||
<a
|
||||
class="text-link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://surveys.modrinth.com/servers-region-waitlist"
|
||||
>
|
||||
<component :is="() => children" />
|
||||
</a>
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
</div>
|
||||
<template v-if="!hideRegionSelection">
|
||||
<h2 class="mt-0 mb-4 text-xl font-bold text-contrast">
|
||||
{{ formatMessage(messages.prompt) }}
|
||||
</h2>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<ServersRegionButton
|
||||
v-for="region in visibleRegions"
|
||||
:key="region.shortcode"
|
||||
v-model="selectedRegion"
|
||||
:region="region"
|
||||
:out-of-stock="currentStock[region.shortcode] === 0"
|
||||
:ping="pings.find((p) => p.region === region.shortcode)?.ping"
|
||||
:best-ping="bestPing === region.shortcode"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-3 text-sm">
|
||||
<IntlFormatted :message-id="messages.regionUnsupported">
|
||||
<template #link="{ children }">
|
||||
<a
|
||||
class="text-link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://surveys.modrinth.com/servers-region-waitlist"
|
||||
>
|
||||
<component :is="() => children" />
|
||||
</a>
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="custom">
|
||||
<h2 class="mt-4 mb-2 text-xl font-bold text-contrast">
|
||||
{{ formatMessage(messages.customPrompt) }}
|
||||
<h2
|
||||
class="mb-2 text-xl font-bold text-contrast"
|
||||
:class="hideRegionSelection ? 'mt-0' : 'mt-4'"
|
||||
>
|
||||
{{
|
||||
formatMessage(hideRegionSelection ? messages.customPromptRamOnly : messages.customPrompt)
|
||||
}}
|
||||
</h2>
|
||||
<div>
|
||||
<Slider v-model="selectedRam" :min="minRam" :max="maxRam" :step="2" unit="GB" />
|
||||
|
||||
@@ -18,8 +18,8 @@ import { getPriceForInterval } from '../utils/product-utils'
|
||||
|
||||
export const useStripe = (
|
||||
publishableKey: string,
|
||||
customer: Stripe.Customer,
|
||||
paymentMethods: Stripe.PaymentMethod[],
|
||||
customer: Ref<Stripe.Customer | null | undefined>,
|
||||
paymentMethods: Ref<Stripe.PaymentMethod[]>,
|
||||
currency: string,
|
||||
product: Ref<Labrinth.Billing.Internal.Product | undefined>,
|
||||
interval: Ref<ServerBillingInterval>,
|
||||
@@ -96,7 +96,7 @@ export const useStripe = (
|
||||
|
||||
const contacts: ContactOption[] = []
|
||||
|
||||
paymentMethods.forEach((method) => {
|
||||
paymentMethods.value.forEach((method) => {
|
||||
const addr = method.billing_details?.address
|
||||
if (
|
||||
addr &&
|
||||
@@ -131,15 +131,22 @@ export const useStripe = (
|
||||
}
|
||||
|
||||
const primaryPaymentMethodId = computed<string | null>(() => {
|
||||
if (customer && customer.invoice_settings && customer.invoice_settings.default_payment_method) {
|
||||
const method = customer.invoice_settings.default_payment_method
|
||||
const customerValue = customer.value
|
||||
const paymentMethodsValue = paymentMethods.value
|
||||
|
||||
if (
|
||||
customerValue &&
|
||||
customerValue.invoice_settings &&
|
||||
customerValue.invoice_settings.default_payment_method
|
||||
) {
|
||||
const method = customerValue.invoice_settings.default_payment_method
|
||||
if (typeof method === 'string') {
|
||||
return method
|
||||
} else {
|
||||
return method.id
|
||||
}
|
||||
} else if (paymentMethods && paymentMethods[0] && paymentMethods[0].id) {
|
||||
return paymentMethods[0].id
|
||||
} else if (paymentMethodsValue[0] && paymentMethodsValue[0].id) {
|
||||
return paymentMethodsValue[0].id
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
@@ -148,7 +155,7 @@ export const useStripe = (
|
||||
const loadStripeElements = async () => {
|
||||
loadingFailed.value = undefined
|
||||
try {
|
||||
if (!customer && primaryPaymentMethodId.value) {
|
||||
if (!customer.value && primaryPaymentMethodId.value) {
|
||||
paymentMethodLoading.value = true
|
||||
await refreshPaymentIntent(primaryPaymentMethodId.value, false)
|
||||
paymentMethodLoading.value = false
|
||||
@@ -189,7 +196,7 @@ export const useStripe = (
|
||||
try {
|
||||
paymentMethodLoading.value = true
|
||||
if (!confirmation) {
|
||||
selectedPaymentMethod.value = paymentMethods.find((x) => x.id === id)
|
||||
selectedPaymentMethod.value = paymentMethods.value.find((x) => x.id === id)
|
||||
}
|
||||
|
||||
if (!product.value) {
|
||||
@@ -248,7 +255,7 @@ export const useStripe = (
|
||||
}
|
||||
).payment_method
|
||||
if (typeof paymentMethod === 'string') {
|
||||
const method = paymentMethods.find((x) => x.id === paymentMethod)
|
||||
const method = paymentMethods.value.find((x) => x.id === paymentMethod)
|
||||
if (method) {
|
||||
inputtedPaymentMethod.value = method
|
||||
}
|
||||
|
||||
@@ -3044,6 +3044,9 @@
|
||||
"servers.region.custom.prompt": {
|
||||
"defaultMessage": "How much RAM do you want your server to have?"
|
||||
},
|
||||
"servers.region.custom.prompt-ram-only": {
|
||||
"defaultMessage": "RAM"
|
||||
},
|
||||
"servers.region.north-america": {
|
||||
"defaultMessage": "North America"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user