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(); const route = useRoute();
// TODO: REMOVE BEFORE MERGE
console.log(props.error);
watch(route, () => { watch(route, () => {
console.log(route); console.log(route);
}); });

View File

@ -344,6 +344,21 @@
Upgrade Upgrade
</button> </button>
</ButtonStyled> </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 <ButtonStyled
v-else-if=" v-else-if="
getPyroCharge(subscription) && getPyroCharge(subscription) &&
@ -403,6 +418,31 @@
:payment-methods="paymentMethods" :payment-methods="paymentMethods"
:return-url="`${config.public.siteUrl}/settings/billing`" :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 <PurchaseModal
ref="pyroPurchaseModal" ref="pyroPurchaseModal"
:product="upgradeProducts" :product="upgradeProducts"
@ -814,6 +854,18 @@ const oppositeInterval = computed(() =>
midasCharge.value?.subscription_interval === "yearly" ? "monthly" : "yearly", 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) { async function switchMidasInterval(interval) {
changingInterval.value = true; changingInterval.value = true;
startLoading(); startLoading();
@ -933,6 +985,7 @@ const getProductPrice = (product, interval) => {
const modalCancel = ref(null); const modalCancel = ref(null);
const pyroPurchaseModal = ref(); const pyroPurchaseModal = ref();
const pyroIntervalModal = ref();
const currentSubscription = ref(null); const currentSubscription = ref(null);
const currentProduct = ref(null); const currentProduct = ref(null);
const upgradeProducts = ref([]); const upgradeProducts = ref([]);

View File

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