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