Merge 46fdb29ba6e0c9e70752630146439c72cd18d830 into d22c9e24f4ca63c8757af0e0d9640f5d0431e815

This commit is contained in:
IMB11 2025-08-07 13:08:22 +01:00 committed by GitHub
commit d482a658c3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 158 additions and 68 deletions

View File

@ -77,6 +77,9 @@ const errorMessages = computed(
const route = useRoute();
// TODO: REMOVE BEFORE MERGE
console.log(props.error);
watch(route, () => {
console.log(route);
});

View File

@ -344,6 +344,21 @@
Upgrade
</button>
</ButtonStyled>
<ButtonStyled
v-if="
getPyroCharge(subscription) &&
getPyroCharge(subscription).status !== 'cancelled' &&
getPyroCharge(subscription).status !== 'failed'
"
color="purple"
color-fill="text"
>
<button @click="showPyroIntervalChange(subscription)">
<TransferIcon />
<!-- TODO: Make this attractive af for monthly subscribers -->
Change billing interval
</button>
</ButtonStyled>
<ButtonStyled
v-else-if="
getPyroCharge(subscription) &&
@ -403,6 +418,31 @@
:payment-methods="paymentMethods"
:return-url="`${config.public.siteUrl}/settings/billing`"
/>
<PurchaseModal
v-if="currentProduct"
ref="pyroIntervalModal"
:product="[currentProduct]"
:country="country"
custom-server
interval-change-only
:existing-subscription="currentSubscription"
:existing-plan="currentProduct"
:publishable-key="config.public.stripePublishableKey"
:send-billing-request="
async (body) => {
await useBaseFetch(`billing/subscription/${currentSubscription.id}`, {
internal: true,
method: 'PATCH',
body,
});
}
"
:renewal-date="currentSubRenewalDate"
:on-error="handleError"
:customer="customer"
:payment-methods="paymentMethods"
:return-url="`${config.public.siteUrl}/settings/billing`"
/>
<PurchaseModal
ref="pyroPurchaseModal"
:product="upgradeProducts"
@ -814,6 +854,18 @@ const oppositeInterval = computed(() =>
midasCharge.value?.subscription_interval === "yearly" ? "monthly" : "yearly",
);
async function showPyroIntervalChange(subscription) {
currentSubscription.value = subscription;
currentSubRenewalDate.value = getPyroCharge(subscription).due;
currentProduct.value = getPyroProduct(subscription);
upgradeProducts.value = [currentProduct.value];
upgradeProducts.value.metadata = { type: "pyro" };
await nextTick();
pyroIntervalModal.value.show();
}
async function switchMidasInterval(interval) {
changingInterval.value = true;
startLoading();
@ -933,6 +985,7 @@ const getProductPrice = (product, interval) => {
const modalCancel = ref(null);
const pyroPurchaseModal = ref();
const pyroIntervalModal = ref();
const currentSubscription = ref(null);
const currentProduct = ref(null);
const upgradeProducts = ref([]);

View File

@ -2,7 +2,8 @@
<NewModal ref="purchaseModal">
<template #title>
<span class="text-contrast text-xl font-extrabold">
<template v-if="productType === 'midas'">Subscribe to Modrinth+!</template>
<template v-if="intervalChangeOnly"> Change billing interval </template>
<template v-else-if="productType === 'midas'">Subscribe to Modrinth+!</template>
<template v-else-if="productType === 'pyro'">
<template v-if="existingSubscription"> Upgrade server plan </template>
<template v-else> Subscribe to Modrinth Servers! </template>
@ -11,11 +12,11 @@
</span>
</template>
<div class="flex items-center gap-1 pb-4">
<template v-if="productType === 'pyro' && !projectId">
<template v-if="!props.intervalChangeOnly && productType === 'pyro' && !projectId">
<span
:class="{
'text-secondary': purchaseModalStep !== 0,
'font-bold': purchaseModalStep === 0,
'text-secondary': !isConfigStep,
'font-bold': isConfigStep,
}"
>
Configure
@ -25,8 +26,8 @@
</template>
<span
:class="{
'text-secondary': purchaseModalStep !== (productType === 'pyro' && !projectId ? 1 : 0),
'font-bold': purchaseModalStep === (productType === 'pyro' && !projectId ? 1 : 0),
'text-secondary': !isBillingStep,
'font-bold': isBillingStep,
}"
>
{{ productType === 'pyro' ? 'Billing' : 'Plan' }}
@ -37,8 +38,8 @@
<ChevronRightIcon class="h-5 w-5 text-secondary" />
<span
:class="{
'text-secondary': purchaseModalStep !== (productType === 'pyro' && !projectId ? 2 : 1),
'font-bold': purchaseModalStep === (productType === 'pyro' && !projectId ? 2 : 1),
'text-secondary': !isPaymentStep,
'font-bold': isPaymentStep,
}"
>
Payment
@ -46,17 +47,14 @@
<ChevronRightIcon class="h-5 w-5 text-secondary" />
<span
:class="{
'text-secondary': purchaseModalStep !== (productType === 'pyro' && !projectId ? 3 : 2),
'font-bold': purchaseModalStep === (productType === 'pyro' && !projectId ? 3 : 2),
'text-secondary': !isReviewStep,
'font-bold': isReviewStep,
}"
>
Review
</span>
</div>
<div
v-if="productType === 'pyro' && !projectId && purchaseModalStep === 0"
class="md:w-[600px] flex flex-col gap-4"
>
<div v-if="!props.intervalChangeOnly && isConfigStep" class="md:w-[600px] flex flex-col gap-4">
<div v-if="!existingSubscription">
<p class="my-2 text-lg font-bold">Configure your server</p>
<div class="flex flex-col gap-4">
@ -182,10 +180,7 @@
</div>
</div>
</div>
<div
v-if="purchaseModalStep === (mutatedProduct.metadata.type === 'pyro' && !projectId ? 1 : 0)"
class="md:w-[600px]"
>
<div v-if="isBillingStep" class="md:w-[600px]">
<div>
<p class="my-2 text-lg font-bold">Choose billing interval</p>
<div v-if="existingPlan" class="flex flex-col gap-3 mb-4 text-secondary">
@ -249,9 +244,7 @@
</div>
</div>
</div>
<template
v-if="purchaseModalStep === (mutatedProduct.metadata.type === 'pyro' && !projectId ? 2 : 1)"
>
<template v-if="isPaymentStep">
<div
v-show="loadingPaymentMethodModal !== 2"
class="flex min-h-[16rem] items-center justify-center md:w-[600px]"
@ -263,12 +256,20 @@
<div id="payment-element" class="mt-4"></div>
</div>
</template>
<div
v-if="purchaseModalStep === (mutatedProduct.metadata.type === 'pyro' && !projectId ? 3 : 2)"
class="md:w-[650px]"
>
<div v-if="isReviewStep" class="md:w-[650px]">
<div v-if="props.intervalChangeOnly" class="r-4 rounded-xl bg-bg p-4">
<p class="my-2 text-lg font-bold text-primary">Billing interval change</p>
<div class="mb-2 flex justify-between">
<span class="text-secondary">
Current interval: {{ props.existingSubscription?.interval }}
</span>
</div>
<div class="mb-2 flex justify-between">
<span class="text-secondary"> New interval: {{ selectedPlan }} </span>
</div>
</div>
<div
v-if="mutatedProduct.metadata.type === 'pyro' && !existingSubscription"
v-else-if="mutatedProduct.metadata.type === 'pyro' && !existingSubscription"
class="r-4 rounded-xl bg-bg p-4 mb-4"
>
<p class="my-2 text-lg font-bold text-primary">Server details</p>
@ -429,7 +430,7 @@
</p>
</div>
<div class="input-group push-right pt-4">
<template v-if="purchaseModalStep === 0">
<template v-if="!props.intervalChangeOnly && purchaseModalStep === 0">
<button class="btn" @click="$refs.purchaseModal.hide()">
<XIcon />
Cancel
@ -454,40 +455,21 @@
</template>
</button>
</template>
<template
v-else-if="
purchaseModalStep === (mutatedProduct.metadata.type === 'pyro' && !projectId ? 1 : 0)
"
>
<button
class="btn"
@click="
purchaseModalStep =
mutatedProduct.metadata.type === 'pyro' && !projectId ? 0 : purchaseModalStep
"
>
<template v-else-if="isBillingStep">
<button v-if="!props.intervalChangeOnly" class="btn" @click="purchaseModalStep = 0">
Back
</button>
<button class="btn btn-primary" :disabled="paymentLoading" @click="beginPurchaseFlow(true)">
<button
class="btn btn-primary"
:disabled="paymentLoading || isSameInterval"
@click="beginPurchaseFlow(true)"
>
<RightArrowIcon />
Select
</button>
</template>
<template
v-else-if="
purchaseModalStep === (mutatedProduct.metadata.type === 'pyro' && !projectId ? 2 : 1)
"
>
<button
class="btn"
@click="
() => {
purchaseModalStep = mutatedProduct.metadata.type === 'pyro' && !projectId ? 1 : 0
loadingPaymentMethodModal = 0
paymentLoading = false
}
"
>
<template v-else-if="isPaymentStep">
<button class="btn" @click="purchaseModalStep = props.intervalChangeOnly ? 0 : 1">
Back
</button>
<button class="btn btn-primary" :disabled="paymentLoading" @click="validatePayment">
@ -495,11 +477,7 @@
Continue
</button>
</template>
<template
v-else-if="
purchaseModalStep === (mutatedProduct.metadata.type === 'pyro' && !projectId ? 3 : 2)
"
>
<template v-else-if="isReviewStep">
<button class="btn" @click="$refs.purchaseModal.hide()">
<XIcon />
Cancel
@ -511,7 +489,7 @@
@click="submitPayment"
>
<CheckCircleIcon />
Subscribe
{{ props.intervalChangeOnly ? 'Change Interval' : 'Subscribe' }}
</button>
<!-- Default Subscribe Button, so M+ still works -->
<button v-else class="btn btn-primary" :disabled="paymentLoading" @click="submitPayment">
@ -639,10 +617,40 @@ const props = defineProps({
required: false,
default: null,
},
intervalChangeOnly: {
type: Boolean,
default: false,
},
})
const productType = computed(() => (props.customServer ? 'pyro' : props.product.metadata.type))
const isConfigStep = computed(
() =>
productType.value === 'pyro' &&
!props.projectId &&
!props.intervalChangeOnly &&
purchaseModalStep.value === 0,
)
const isBillingStep = computed(
() =>
purchaseModalStep.value ===
(props.intervalChangeOnly ? 0 : productType.value === 'pyro' && !props.projectId ? 1 : 0),
)
const isPaymentStep = computed(
() =>
purchaseModalStep.value ===
(props.intervalChangeOnly ? 1 : productType.value === 'pyro' && !props.projectId ? 2 : 1),
)
const isReviewStep = computed(
() =>
purchaseModalStep.value ===
(props.intervalChangeOnly ? 2 : productType.value === 'pyro' && !props.projectId ? 3 : 2),
)
const messages = defineMessages({
paymentMethodCardDisplay: {
id: 'omorphia.component.purchase_modal.payment_method_card_display',
@ -749,6 +757,10 @@ const customServerConfig = reactive({
})
const updateCustomServerProduct = () => {
if (!props.product || !Array.isArray(props.product)) {
return
}
customMatchingProduct.value = props.product.find(
(product) => product.metadata.ram === customServerConfig.ram,
)
@ -789,6 +801,10 @@ const updateCustomServerStock = async () => {
}
function updateRamValues() {
if (!props.product || !Array.isArray(props.product)) {
return
}
const ramValues = props.product.map((product) => product.metadata.ram / 1024)
customMinRam.value = Math.min(...ramValues)
customMaxRam.value = Math.max(...ramValues)
@ -868,10 +884,19 @@ const sharedCpus = computed(() => {
return (mutatedProduct.value?.metadata?.cpu ?? 0) / 2
})
const isSameInterval = computed(() => {
return (
props.intervalChangeOnly &&
props.existingSubscription &&
selectedPlan.value === props.existingSubscription.interval
)
})
function nextStep() {
if (
mutatedProduct.value.metadata.type === 'pyro' &&
!props.projectId &&
!props.intervalChangeOnly &&
purchaseModalStep.value === 0
) {
purchaseModalStep.value = 1
@ -891,13 +916,19 @@ async function beginPurchaseFlow(skip = false) {
paymentLoading.value = true
await refreshPayment(null, primaryPaymentMethodId.value)
paymentLoading.value = false
purchaseModalStep.value =
mutatedProduct.value.metadata.type === 'pyro' && !props.projectId ? 3 : 2
purchaseModalStep.value = props.intervalChangeOnly
? 2
: mutatedProduct.value.metadata.type === 'pyro' && !props.projectId
? 3
: 2
} else {
try {
loadingPaymentMethodModal.value = 0
purchaseModalStep.value =
mutatedProduct.value.metadata.type === 'pyro' && !props.projectId ? 2 : 1
purchaseModalStep.value = props.intervalChangeOnly
? 1
: mutatedProduct.value.metadata.type === 'pyro' && !props.projectId
? 2
: 1
await nextTick()
@ -955,8 +986,11 @@ async function validatePayment() {
loadingPaymentMethodModal.value = 0
confirmationToken.value = await createConfirmationToken()
purchaseModalStep.value =
mutatedProduct.value.metadata.type === 'pyro' && !props.projectId ? 3 : 2
purchaseModalStep.value = props.intervalChangeOnly
? 2
: mutatedProduct.value.metadata.type === 'pyro' && !props.projectId
? 3
: 2
paymentLoading.value = false
}
@ -1006,7 +1040,7 @@ async function refreshPayment(confirmationId, paymentMethodId) {
},
)
if (!paymentIntentId.value) {
if (result.payment_intent_id && !paymentIntentId.value) {
paymentIntentId.value = result.payment_intent_id
clientSecret.value = result.client_secret
}