You've already forked AstralRinth
forked from didirus/AstralRinth
Initial servers upgrades frontend (#3219)
* Initial servers upgrades frontend * Fix error when purchasing non-custom servers * fix backend * Fix comment --------- Signed-off-by: Jai Agrawal <18202329+Geometrically@users.noreply.github.com> Co-authored-by: Jai A <jaiagr+gpg@pm.me> Co-authored-by: Jai Agrawal <18202329+Geometrically@users.noreply.github.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="universal-card">
|
<section class="universal-card experimental-styles-within">
|
||||||
<h2>{{ formatMessage(messages.subscriptionTitle) }}</h2>
|
<h2>{{ formatMessage(messages.subscriptionTitle) }}</h2>
|
||||||
<p>{{ formatMessage(messages.subscriptionDescription) }}</p>
|
<p>{{ formatMessage(messages.subscriptionDescription) }}</p>
|
||||||
<div class="universal-card recessed">
|
<div class="universal-card recessed">
|
||||||
@@ -88,67 +88,69 @@
|
|||||||
v-if="midasCharge && midasCharge.status === 'failed'"
|
v-if="midasCharge && midasCharge.status === 'failed'"
|
||||||
class="ml-auto flex flex-row-reverse items-center gap-2"
|
class="ml-auto flex flex-row-reverse items-center gap-2"
|
||||||
>
|
>
|
||||||
|
<ButtonStyled v-if="midasCharge && midasCharge.status === 'failed'">
|
||||||
|
<button
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
$refs.midasPurchaseModal.show();
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<UpdatedIcon />
|
||||||
|
Update method
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<ButtonStyled type="transparent" circular>
|
||||||
|
<OverflowMenu
|
||||||
|
:dropdown-id="`${baseId}-cancel-midas`"
|
||||||
|
:options="[
|
||||||
|
{
|
||||||
|
id: 'cancel',
|
||||||
|
action: () => {
|
||||||
|
cancelSubscriptionId = midasSubscription.id;
|
||||||
|
$refs.modalCancel.show();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<MoreVerticalIcon />
|
||||||
|
<template #cancel><XIcon /> Cancel</template>
|
||||||
|
</OverflowMenu>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
<ButtonStyled v-else-if="midasCharge && midasCharge.status !== 'cancelled'">
|
||||||
<button
|
<button
|
||||||
v-if="midasCharge && midasCharge.status === 'failed'"
|
class="ml-auto"
|
||||||
class="iconified-button raised-button"
|
|
||||||
@click="
|
@click="
|
||||||
() => {
|
() => {
|
||||||
purchaseModalStep = 0;
|
cancelSubscriptionId = midasSubscription.id;
|
||||||
$refs.purchaseModal.show();
|
$refs.modalCancel.show();
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<UpdatedIcon />
|
<XIcon /> Cancel
|
||||||
Update method
|
|
||||||
</button>
|
</button>
|
||||||
<OverflowMenu
|
</ButtonStyled>
|
||||||
class="btn icon-only transparent"
|
<ButtonStyled
|
||||||
:options="[
|
|
||||||
{
|
|
||||||
id: 'cancel',
|
|
||||||
action: () => {
|
|
||||||
cancelSubscriptionId = midasSubscription.id;
|
|
||||||
$refs.modalCancel.show();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<MoreVerticalIcon />
|
|
||||||
<template #cancel><XIcon /> Cancel</template>
|
|
||||||
</OverflowMenu>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
v-else-if="midasCharge && midasCharge.status !== 'cancelled'"
|
|
||||||
class="iconified-button raised-button !ml-auto"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
cancelSubscriptionId = midasSubscription.id;
|
|
||||||
$refs.modalCancel.show();
|
|
||||||
}
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<XIcon /> Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-else-if="midasCharge && midasCharge.status === 'cancelled'"
|
v-else-if="midasCharge && midasCharge.status === 'cancelled'"
|
||||||
class="btn btn-purple btn-large ml-auto"
|
color="purple"
|
||||||
@click="cancelSubscription(midasSubscription.id, false)"
|
|
||||||
>
|
>
|
||||||
<RightArrowIcon /> Resubscribe
|
<button class="ml-auto" @click="cancelSubscription(midasSubscription.id, false)">
|
||||||
</button>
|
Resubscribe <RightArrowIcon />
|
||||||
<button
|
</button>
|
||||||
v-else
|
</ButtonStyled>
|
||||||
class="btn btn-purple btn-large ml-auto"
|
<ButtonStyled v-else color="purple" size="large">
|
||||||
@click="
|
<button
|
||||||
() => {
|
class="ml-auto"
|
||||||
purchaseModalStep = 0;
|
@click="
|
||||||
$refs.purchaseModal.show();
|
() => {
|
||||||
}
|
$refs.midasPurchaseModal.show();
|
||||||
"
|
}
|
||||||
>
|
"
|
||||||
<RightArrowIcon />
|
>
|
||||||
Subscribe
|
Subscribe <RightArrowIcon />
|
||||||
</button>
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -282,25 +284,37 @@
|
|||||||
getPyroCharge(subscription).status !== 'cancelled' &&
|
getPyroCharge(subscription).status !== 'cancelled' &&
|
||||||
getPyroCharge(subscription).status !== 'failed'
|
getPyroCharge(subscription).status !== 'failed'
|
||||||
"
|
"
|
||||||
type="standard"
|
|
||||||
@click="showPyroCancelModal(subscription.id)"
|
|
||||||
>
|
>
|
||||||
<button class="text-contrast">
|
<button @click="showPyroCancelModal(subscription.id)">
|
||||||
<XIcon />
|
<XIcon />
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
|
<ButtonStyled
|
||||||
|
v-if="
|
||||||
|
getPyroCharge(subscription) &&
|
||||||
|
getPyroCharge(subscription).status !== 'cancelled' &&
|
||||||
|
getPyroCharge(subscription).status !== 'failed'
|
||||||
|
"
|
||||||
|
color="green"
|
||||||
|
color-fill="text"
|
||||||
|
>
|
||||||
|
<button @click="showPyroUpgradeModal(subscription)">
|
||||||
|
<ArrowBigUpDashIcon />
|
||||||
|
Upgrade
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
<ButtonStyled
|
<ButtonStyled
|
||||||
v-else-if="
|
v-else-if="
|
||||||
getPyroCharge(subscription) &&
|
getPyroCharge(subscription) &&
|
||||||
(getPyroCharge(subscription).status === 'cancelled' ||
|
(getPyroCharge(subscription).status === 'cancelled' ||
|
||||||
getPyroCharge(subscription).status === 'failed')
|
getPyroCharge(subscription).status === 'failed')
|
||||||
"
|
"
|
||||||
type="standard"
|
|
||||||
color="green"
|
color="green"
|
||||||
@click="resubscribePyro(subscription.id)"
|
|
||||||
>
|
>
|
||||||
<button class="text-contrast">Resubscribe</button>
|
<button @click="resubscribePyro(subscription.id)">
|
||||||
|
Resubscribe <RightArrowIcon />
|
||||||
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -312,7 +326,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="universal-card">
|
<section class="universal-card experimental-styles-within">
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
ref="modal_confirm"
|
ref="modal_confirm"
|
||||||
:title="formatMessage(deleteModalMessages.title)"
|
:title="formatMessage(deleteModalMessages.title)"
|
||||||
@@ -321,7 +335,7 @@
|
|||||||
@proceed="removePaymentMethod(removePaymentMethodIndex)"
|
@proceed="removePaymentMethod(removePaymentMethodIndex)"
|
||||||
/>
|
/>
|
||||||
<PurchaseModal
|
<PurchaseModal
|
||||||
ref="purchaseModal"
|
ref="midasPurchaseModal"
|
||||||
:product="midasProduct"
|
:product="midasProduct"
|
||||||
:country="country"
|
:country="country"
|
||||||
:publishable-key="config.public.stripePublishableKey"
|
:publishable-key="config.public.stripePublishableKey"
|
||||||
@@ -342,6 +356,37 @@
|
|||||||
:payment-methods="paymentMethods"
|
:payment-methods="paymentMethods"
|
||||||
:return-url="`${config.public.siteUrl}/settings/billing`"
|
:return-url="`${config.public.siteUrl}/settings/billing`"
|
||||||
/>
|
/>
|
||||||
|
<PurchaseModal
|
||||||
|
ref="pyroPurchaseModal"
|
||||||
|
:product="upgradeProducts"
|
||||||
|
:country="country"
|
||||||
|
custom-server
|
||||||
|
: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: body,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
:renewal-date="currentSubRenewalDate"
|
||||||
|
:on-error="
|
||||||
|
(err) =>
|
||||||
|
data.$notify({
|
||||||
|
group: 'main',
|
||||||
|
title: 'An error occurred',
|
||||||
|
type: 'error',
|
||||||
|
text: err.message ?? (err.data ? err.data.description : err),
|
||||||
|
})
|
||||||
|
"
|
||||||
|
:customer="customer"
|
||||||
|
:payment-methods="paymentMethods"
|
||||||
|
:return-url="`${config.public.siteUrl}/servers/manage`"
|
||||||
|
:server-name="`${auth?.user?.username}'s server`"
|
||||||
|
/>
|
||||||
<NewModal ref="addPaymentMethodModal">
|
<NewModal ref="addPaymentMethodModal">
|
||||||
<template #title>
|
<template #title>
|
||||||
<span class="text-lg font-extrabold text-contrast">
|
<span class="text-lg font-extrabold text-contrast">
|
||||||
@@ -359,15 +404,19 @@
|
|||||||
<div id="address-element"></div>
|
<div id="address-element"></div>
|
||||||
<div id="payment-element" class="mt-4"></div>
|
<div id="payment-element" class="mt-4"></div>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="loadingPaymentMethodModal === 2" class="input-group push-right mt-auto pt-4">
|
<div v-show="loadingPaymentMethodModal === 2" class="input-group mt-auto pt-4">
|
||||||
<button class="btn" @click="$refs.addPaymentMethodModal.hide()">
|
<ButtonStyled color="brand">
|
||||||
<XIcon />
|
<button :disabled="loadingAddMethod" @click="submit">
|
||||||
{{ formatMessage(commonMessages.cancelButton) }}
|
<PlusIcon />
|
||||||
</button>
|
{{ formatMessage(messages.paymentMethodAdd) }}
|
||||||
<button class="btn btn-primary" :disabled="loadingAddMethod" @click="submit">
|
</button>
|
||||||
<PlusIcon />
|
</ButtonStyled>
|
||||||
{{ formatMessage(messages.paymentMethodAdd) }}
|
<ButtonStyled>
|
||||||
</button>
|
<button @click="$refs.addPaymentMethodModal.hide()">
|
||||||
|
<XIcon />
|
||||||
|
{{ formatMessage(commonMessages.cancelButton) }}
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</NewModal>
|
</NewModal>
|
||||||
@@ -442,6 +491,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<OverflowMenu
|
<OverflowMenu
|
||||||
|
:dropdown-id="`${baseId}-payment-method-overflow-${index}`"
|
||||||
class="btn icon-only transparent"
|
class="btn icon-only transparent"
|
||||||
:options="
|
:options="
|
||||||
[
|
[
|
||||||
@@ -493,6 +543,7 @@ import {
|
|||||||
} from "@modrinth/ui";
|
} from "@modrinth/ui";
|
||||||
import {
|
import {
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
|
ArrowBigUpDashIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
CardIcon,
|
CardIcon,
|
||||||
MoreVerticalIcon,
|
MoreVerticalIcon,
|
||||||
@@ -515,6 +566,9 @@ definePageMeta({
|
|||||||
middleware: "auth",
|
middleware: "auth",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const auth = await useAuth();
|
||||||
|
const baseId = useId();
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
script: [
|
script: [
|
||||||
{
|
{
|
||||||
@@ -704,7 +758,7 @@ const pyroSubscriptions = computed(() => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const purchaseModal = ref();
|
const midasPurchaseModal = ref();
|
||||||
const country = useUserCountry();
|
const country = useUserCountry();
|
||||||
const price = computed(() =>
|
const price = computed(() =>
|
||||||
midasProduct.value?.prices?.find((x) => x.currency_code === getCurrency(country.value)),
|
midasProduct.value?.prices?.find((x) => x.currency_code === getCurrency(country.value)),
|
||||||
@@ -896,6 +950,46 @@ const showPyroCancelModal = (subscriptionId) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const pyroPurchaseModal = ref();
|
||||||
|
const currentSubscription = ref(null);
|
||||||
|
const currentProduct = ref(null);
|
||||||
|
const upgradeProducts = ref([]);
|
||||||
|
upgradeProducts.value.metadata = { type: "pyro" };
|
||||||
|
|
||||||
|
const currentSubRenewalDate = ref();
|
||||||
|
|
||||||
|
const showPyroUpgradeModal = async (subscription) => {
|
||||||
|
currentSubscription.value = subscription;
|
||||||
|
currentSubRenewalDate.value = getPyroCharge(subscription).due;
|
||||||
|
currentProduct.value = getPyroProduct(subscription);
|
||||||
|
upgradeProducts.value = products.filter(
|
||||||
|
(p) =>
|
||||||
|
p.metadata.type === "pyro" &&
|
||||||
|
(!currentProduct.value || p.metadata.ram > currentProduct.value.metadata.ram),
|
||||||
|
);
|
||||||
|
upgradeProducts.value.metadata = { type: "pyro" };
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
if (!currentProduct.value) {
|
||||||
|
console.error("Could not find product for current subscription");
|
||||||
|
data.$notify({
|
||||||
|
group: "main",
|
||||||
|
title: "An error occurred",
|
||||||
|
text: "Could not find product for current subscription",
|
||||||
|
type: "error",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pyroPurchaseModal.value) {
|
||||||
|
console.error("pyroPurchaseModal ref is undefined");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pyroPurchaseModal.value.show();
|
||||||
|
};
|
||||||
|
|
||||||
const resubscribePyro = async (subscriptionId) => {
|
const resubscribePyro = async (subscriptionId) => {
|
||||||
try {
|
try {
|
||||||
await useBaseFetch(`billing/subscription/${subscriptionId}`, {
|
await useBaseFetch(`billing/subscription/${subscriptionId}`, {
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ pub struct Charge {
|
|||||||
pub net: Option<i64>,
|
pub net: Option<i64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq)]
|
||||||
#[serde(tag = "type", rename_all = "kebab-case")]
|
#[serde(tag = "type", rename_all = "kebab-case")]
|
||||||
pub enum ChargeType {
|
pub enum ChargeType {
|
||||||
OneTime,
|
OneTime,
|
||||||
|
|||||||
@@ -411,11 +411,7 @@ pub async fn edit_subscription(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let interval = open_charge.due - Utc::now();
|
let interval = open_charge.due - Utc::now();
|
||||||
let duration = PriceDuration::iterator()
|
let duration = PriceDuration::Monthly;
|
||||||
.min_by_key(|x| {
|
|
||||||
(x.duration().num_seconds() - interval.num_seconds()).abs()
|
|
||||||
})
|
|
||||||
.unwrap_or(PriceDuration::Monthly);
|
|
||||||
|
|
||||||
let current_amount = match ¤t_price.prices {
|
let current_amount = match ¤t_price.prices {
|
||||||
Price::OneTime { price } => *price,
|
Price::OneTime { price } => *price,
|
||||||
@@ -461,23 +457,6 @@ pub async fn edit_subscription(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let charge_id = generate_charge_id(&mut transaction).await?;
|
let charge_id = generate_charge_id(&mut transaction).await?;
|
||||||
let mut charge = ChargeItem {
|
|
||||||
id: charge_id,
|
|
||||||
user_id: user.id.into(),
|
|
||||||
price_id: product_price.id,
|
|
||||||
amount: proration as i64,
|
|
||||||
currency_code: current_price.currency_code.clone(),
|
|
||||||
status: ChargeStatus::Processing,
|
|
||||||
due: Utc::now(),
|
|
||||||
last_attempt: None,
|
|
||||||
type_: ChargeType::Proration,
|
|
||||||
subscription_id: Some(subscription.id),
|
|
||||||
subscription_interval: Some(duration),
|
|
||||||
payment_platform: PaymentPlatform::Stripe,
|
|
||||||
payment_platform_id: None,
|
|
||||||
parent_charge_id: None,
|
|
||||||
net: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let customer_id = get_or_create_customer(
|
let customer_id = get_or_create_customer(
|
||||||
user.id,
|
user.id,
|
||||||
@@ -504,6 +483,30 @@ pub async fn edit_subscription(
|
|||||||
"modrinth_user_id".to_string(),
|
"modrinth_user_id".to_string(),
|
||||||
to_base62(user.id.0),
|
to_base62(user.id.0),
|
||||||
);
|
);
|
||||||
|
metadata.insert(
|
||||||
|
"modrinth_charge_id".to_string(),
|
||||||
|
to_base62(charge_id.0 as u64),
|
||||||
|
);
|
||||||
|
metadata.insert(
|
||||||
|
"modrinth_subscription_id".to_string(),
|
||||||
|
to_base62(subscription.id.0 as u64),
|
||||||
|
);
|
||||||
|
metadata.insert(
|
||||||
|
"modrinth_price_id".to_string(),
|
||||||
|
to_base62(product_price.id.0 as u64),
|
||||||
|
);
|
||||||
|
metadata.insert(
|
||||||
|
"modrinth_subscription_interval".to_string(),
|
||||||
|
open_charge
|
||||||
|
.subscription_interval
|
||||||
|
.unwrap_or(PriceDuration::Monthly)
|
||||||
|
.as_str()
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
metadata.insert(
|
||||||
|
"modrinth_charge_type".to_string(),
|
||||||
|
ChargeType::Proration.as_str().to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
intent.customer = Some(customer_id);
|
intent.customer = Some(customer_id);
|
||||||
intent.metadata = Some(metadata);
|
intent.metadata = Some(metadata);
|
||||||
@@ -529,9 +532,6 @@ pub async fn edit_subscription(
|
|||||||
stripe::PaymentIntent::create(&stripe_client, intent)
|
stripe::PaymentIntent::create(&stripe_client, intent)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
charge.payment_platform_id = Some(intent.id.to_string());
|
|
||||||
charge.upsert(&mut transaction).await?;
|
|
||||||
|
|
||||||
Some((proration, 0, intent))
|
Some((proration, 0, intent))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -1139,7 +1139,7 @@ pub async fn initiate_payment(
|
|||||||
let country = user_country.as_deref().unwrap_or("US");
|
let country = user_country.as_deref().unwrap_or("US");
|
||||||
let recommended_currency_code = infer_currency_code(country);
|
let recommended_currency_code = infer_currency_code(country);
|
||||||
|
|
||||||
let (price, currency_code, interval, price_id, charge_id) =
|
let (price, currency_code, interval, price_id, charge_id, charge_type) =
|
||||||
match payment_request.charge {
|
match payment_request.charge {
|
||||||
ChargeRequestType::Existing { id } => {
|
ChargeRequestType::Existing { id } => {
|
||||||
let charge =
|
let charge =
|
||||||
@@ -1160,6 +1160,7 @@ pub async fn initiate_payment(
|
|||||||
charge.subscription_interval,
|
charge.subscription_interval,
|
||||||
charge.price_id,
|
charge.price_id,
|
||||||
Some(id),
|
Some(id),
|
||||||
|
charge.type_,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
ChargeRequestType::New {
|
ChargeRequestType::New {
|
||||||
@@ -1256,6 +1257,11 @@ pub async fn initiate_payment(
|
|||||||
interval,
|
interval,
|
||||||
price_item.id,
|
price_item.id,
|
||||||
None,
|
None,
|
||||||
|
if let Price::Recurring { .. } = price_item.prices {
|
||||||
|
ChargeType::Subscription
|
||||||
|
} else {
|
||||||
|
ChargeType::OneTime
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1314,6 +1320,11 @@ pub async fn initiate_payment(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
metadata.insert(
|
||||||
|
"modrinth_charge_type".to_string(),
|
||||||
|
charge_type.as_str().to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
if let Some(charge_id) = charge_id {
|
if let Some(charge_id) = charge_id {
|
||||||
metadata.insert(
|
metadata.insert(
|
||||||
"modrinth_charge_id".to_string(),
|
"modrinth_charge_id".to_string(),
|
||||||
@@ -1399,10 +1410,15 @@ pub async fn stripe_webhook(
|
|||||||
pub user_subscription_item:
|
pub user_subscription_item:
|
||||||
Option<user_subscription_item::UserSubscriptionItem>,
|
Option<user_subscription_item::UserSubscriptionItem>,
|
||||||
pub payment_metadata: Option<PaymentRequestMetadata>,
|
pub payment_metadata: Option<PaymentRequestMetadata>,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub charge_type: ChargeType,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
async fn get_payment_intent_metadata(
|
async fn get_payment_intent_metadata(
|
||||||
payment_intent_id: PaymentIntentId,
|
payment_intent_id: PaymentIntentId,
|
||||||
|
amount: i64,
|
||||||
|
currency: String,
|
||||||
metadata: HashMap<String, String>,
|
metadata: HashMap<String, String>,
|
||||||
pool: &PgPool,
|
pool: &PgPool,
|
||||||
redis: &RedisPool,
|
redis: &RedisPool,
|
||||||
@@ -1445,6 +1461,15 @@ pub async fn stripe_webhook(
|
|||||||
break 'metadata;
|
break 'metadata;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let charge_type = if let Some(charge_type) = metadata
|
||||||
|
.get("modrinth_charge_type")
|
||||||
|
.map(|x| ChargeType::from_string(x))
|
||||||
|
{
|
||||||
|
charge_type
|
||||||
|
} else {
|
||||||
|
break 'metadata;
|
||||||
|
};
|
||||||
|
|
||||||
let (charge, price, product, subscription) = if let Some(
|
let (charge, price, product, subscription) = if let Some(
|
||||||
mut charge,
|
mut charge,
|
||||||
) =
|
) =
|
||||||
@@ -1549,8 +1574,8 @@ pub async fn stripe_webhook(
|
|||||||
break 'metadata;
|
break 'metadata;
|
||||||
};
|
};
|
||||||
|
|
||||||
let (amount, subscription) = match &price.prices {
|
let subscription = match &price.prices {
|
||||||
Price::OneTime { price } => (*price, None),
|
Price::OneTime { .. } => None,
|
||||||
Price::Recurring { intervals } => {
|
Price::Recurring { intervals } => {
|
||||||
let interval = if let Some(interval) = metadata
|
let interval = if let Some(interval) = metadata
|
||||||
.get("modrinth_subscription_interval")
|
.get("modrinth_subscription_interval")
|
||||||
@@ -1561,7 +1586,7 @@ pub async fn stripe_webhook(
|
|||||||
break 'metadata;
|
break 'metadata;
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(price) = intervals.get(&interval) {
|
if intervals.get(&interval).is_some() {
|
||||||
let subscription_id = if let Some(subscription_id) = metadata
|
let subscription_id = if let Some(subscription_id) = metadata
|
||||||
.get("modrinth_subscription_id")
|
.get("modrinth_subscription_id")
|
||||||
.and_then(|x| parse_base62(x).ok())
|
.and_then(|x| parse_base62(x).ok())
|
||||||
@@ -1573,21 +1598,29 @@ pub async fn stripe_webhook(
|
|||||||
break 'metadata;
|
break 'metadata;
|
||||||
};
|
};
|
||||||
|
|
||||||
let subscription = user_subscription_item::UserSubscriptionItem {
|
let subscription = if let Some(mut subscription) = user_subscription_item::UserSubscriptionItem::get(subscription_id, pool).await? {
|
||||||
id: subscription_id,
|
subscription.status = SubscriptionStatus::Unprovisioned;
|
||||||
user_id,
|
subscription.price_id = price_id;
|
||||||
price_id,
|
subscription.interval = interval;
|
||||||
interval,
|
|
||||||
created: Utc::now(),
|
subscription
|
||||||
status: SubscriptionStatus::Unprovisioned,
|
} else {
|
||||||
metadata: None,
|
user_subscription_item::UserSubscriptionItem {
|
||||||
|
id: subscription_id,
|
||||||
|
user_id,
|
||||||
|
price_id,
|
||||||
|
interval,
|
||||||
|
created: Utc::now(),
|
||||||
|
status: SubscriptionStatus::Unprovisioned,
|
||||||
|
metadata: None,
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if charge_status != ChargeStatus::Failed {
|
if charge_status != ChargeStatus::Failed {
|
||||||
subscription.upsert(transaction).await?;
|
subscription.upsert(transaction).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
(*price, Some(subscription))
|
Some(subscription)
|
||||||
} else {
|
} else {
|
||||||
break 'metadata;
|
break 'metadata;
|
||||||
}
|
}
|
||||||
@@ -1598,16 +1631,12 @@ pub async fn stripe_webhook(
|
|||||||
id: charge_id,
|
id: charge_id,
|
||||||
user_id,
|
user_id,
|
||||||
price_id,
|
price_id,
|
||||||
amount: amount as i64,
|
amount,
|
||||||
currency_code: price.currency_code.clone(),
|
currency_code: currency,
|
||||||
status: charge_status,
|
status: charge_status,
|
||||||
due: Utc::now(),
|
due: Utc::now(),
|
||||||
last_attempt: Some(Utc::now()),
|
last_attempt: Some(Utc::now()),
|
||||||
type_: if subscription.is_some() {
|
type_: charge_type,
|
||||||
ChargeType::Subscription
|
|
||||||
} else {
|
|
||||||
ChargeType::OneTime
|
|
||||||
},
|
|
||||||
subscription_id: subscription.as_ref().map(|x| x.id),
|
subscription_id: subscription.as_ref().map(|x| x.id),
|
||||||
subscription_interval: subscription
|
subscription_interval: subscription
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -1634,6 +1663,7 @@ pub async fn stripe_webhook(
|
|||||||
charge_item: charge,
|
charge_item: charge,
|
||||||
user_subscription_item: subscription,
|
user_subscription_item: subscription,
|
||||||
payment_metadata,
|
payment_metadata,
|
||||||
|
charge_type,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1651,6 +1681,8 @@ pub async fn stripe_webhook(
|
|||||||
|
|
||||||
let mut metadata = get_payment_intent_metadata(
|
let mut metadata = get_payment_intent_metadata(
|
||||||
payment_intent.id,
|
payment_intent.id,
|
||||||
|
payment_intent.amount,
|
||||||
|
payment_intent.currency.to_string().to_uppercase(),
|
||||||
payment_intent.metadata,
|
payment_intent.metadata,
|
||||||
&pool,
|
&pool,
|
||||||
&redis,
|
&redis,
|
||||||
@@ -1899,6 +1931,8 @@ pub async fn stripe_webhook(
|
|||||||
let mut transaction = pool.begin().await?;
|
let mut transaction = pool.begin().await?;
|
||||||
get_payment_intent_metadata(
|
get_payment_intent_metadata(
|
||||||
payment_intent.id,
|
payment_intent.id,
|
||||||
|
payment_intent.amount,
|
||||||
|
payment_intent.currency.to_string().to_uppercase(),
|
||||||
payment_intent.metadata,
|
payment_intent.metadata,
|
||||||
&pool,
|
&pool,
|
||||||
&redis,
|
&redis,
|
||||||
@@ -1917,6 +1951,8 @@ pub async fn stripe_webhook(
|
|||||||
|
|
||||||
let metadata = get_payment_intent_metadata(
|
let metadata = get_payment_intent_metadata(
|
||||||
payment_intent.id,
|
payment_intent.id,
|
||||||
|
payment_intent.amount,
|
||||||
|
payment_intent.currency.to_string().to_uppercase(),
|
||||||
payment_intent.metadata,
|
payment_intent.metadata,
|
||||||
&pool,
|
&pool,
|
||||||
&redis,
|
&redis,
|
||||||
@@ -2320,6 +2356,10 @@ pub async fn task(
|
|||||||
"modrinth_charge_id".to_string(),
|
"modrinth_charge_id".to_string(),
|
||||||
to_base62(charge.id.0 as u64),
|
to_base62(charge.id.0 as u64),
|
||||||
);
|
);
|
||||||
|
metadata.insert(
|
||||||
|
"modrinth_charge_type".to_string(),
|
||||||
|
charge.type_.as_str().to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
intent.metadata = Some(metadata);
|
intent.metadata = Some(metadata);
|
||||||
intent.customer = Some(customer.id);
|
intent.customer = Some(customer.id);
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ async function copyText() {
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.code {
|
.code {
|
||||||
|
color: var(--color-text);
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
grid-gap: 0.5rem;
|
grid-gap: 0.5rem;
|
||||||
font-family: var(--mono-font);
|
font-family: var(--mono-font);
|
||||||
|
|||||||
@@ -2,15 +2,16 @@
|
|||||||
<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="product.metadata.type === 'midas'">Subscribe to Modrinth Plus!</template>
|
<template v-if="productType === 'midas'">Subscribe to Modrinth+!</template>
|
||||||
<template v-else-if="product.metadata.type === 'pyro'"
|
<template v-else-if="productType === 'pyro'">
|
||||||
>Subscribe to Modrinth Servers!</template
|
<template v-if="existingSubscription"> Upgrade server plan </template>
|
||||||
>
|
<template v-else> Subscribe to Modrinth Servers! </template>
|
||||||
|
</template>
|
||||||
<template v-else>Purchase product</template>
|
<template v-else>Purchase product</template>
|
||||||
</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="product.metadata.type === 'pyro' && !projectId">
|
<template v-if="productType === 'pyro' && !projectId">
|
||||||
<span
|
<span
|
||||||
:class="{
|
:class="{
|
||||||
'text-secondary': purchaseModalStep !== 0,
|
'text-secondary': purchaseModalStep !== 0,
|
||||||
@@ -24,24 +25,20 @@
|
|||||||
</template>
|
</template>
|
||||||
<span
|
<span
|
||||||
:class="{
|
:class="{
|
||||||
'text-secondary':
|
'text-secondary': purchaseModalStep !== (productType === 'pyro' && !projectId ? 1 : 0),
|
||||||
purchaseModalStep !== (product.metadata.type === 'pyro' && !projectId ? 1 : 0),
|
'font-bold': purchaseModalStep === (productType === 'pyro' && !projectId ? 1 : 0),
|
||||||
'font-bold':
|
|
||||||
purchaseModalStep === (product.metadata.type === 'pyro' && !projectId ? 1 : 0),
|
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
{{ product.metadata.type === 'pyro' ? 'Billing' : 'Plan' }}
|
{{ productType === 'pyro' ? 'Billing' : 'Plan' }}
|
||||||
<span class="hidden sm:inline">{{
|
<span class="hidden sm:inline">{{
|
||||||
product.metadata.type === 'pyro' ? 'interval' : 'selection'
|
productType === 'pyro' ? 'interval' : 'selection'
|
||||||
}}</span>
|
}}</span>
|
||||||
</span>
|
</span>
|
||||||
<ChevronRightIcon class="h-5 w-5 text-secondary" />
|
<ChevronRightIcon class="h-5 w-5 text-secondary" />
|
||||||
<span
|
<span
|
||||||
:class="{
|
:class="{
|
||||||
'text-secondary':
|
'text-secondary': purchaseModalStep !== (productType === 'pyro' && !projectId ? 2 : 1),
|
||||||
purchaseModalStep !== (product.metadata.type === 'pyro' && !projectId ? 2 : 1),
|
'font-bold': purchaseModalStep === (productType === 'pyro' && !projectId ? 2 : 1),
|
||||||
'font-bold':
|
|
||||||
purchaseModalStep === (product.metadata.type === 'pyro' && !projectId ? 2 : 1),
|
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
Payment
|
Payment
|
||||||
@@ -49,20 +46,18 @@
|
|||||||
<ChevronRightIcon class="h-5 w-5 text-secondary" />
|
<ChevronRightIcon class="h-5 w-5 text-secondary" />
|
||||||
<span
|
<span
|
||||||
:class="{
|
:class="{
|
||||||
'text-secondary':
|
'text-secondary': purchaseModalStep !== (productType === 'pyro' && !projectId ? 3 : 2),
|
||||||
purchaseModalStep !== (product.metadata.type === 'pyro' && !projectId ? 3 : 2),
|
'font-bold': purchaseModalStep === (productType === 'pyro' && !projectId ? 3 : 2),
|
||||||
'font-bold':
|
|
||||||
purchaseModalStep === (product.metadata.type === 'pyro' && !projectId ? 3 : 2),
|
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
Review
|
Review
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="product.metadata.type === 'pyro' && !projectId && purchaseModalStep === 0"
|
v-if="productType === 'pyro' && !projectId && purchaseModalStep === 0"
|
||||||
class="md:w-[600px] flex flex-col gap-4"
|
class="md:w-[600px] flex flex-col gap-4"
|
||||||
>
|
>
|
||||||
<div>
|
<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">
|
||||||
<input v-model="serverName" placeholder="Server name" class="input" maxlength="48" />
|
<input v-model="serverName" placeholder="Server name" class="input" maxlength="48" />
|
||||||
@@ -105,13 +100,20 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="customServer">
|
<div v-if="customServer">
|
||||||
<div class="flex gap-2 items-center">
|
<div class="flex gap-2 items-center">
|
||||||
<p class="my-2 text-lg font-bold">Configure your RAM</p>
|
<p class="my-2 text-lg font-bold">
|
||||||
|
<template v-if="existingSubscription">Upgrade your RAM</template>
|
||||||
|
<template v-else>Configure your RAM</template>
|
||||||
|
</p>
|
||||||
<IssuesIcon
|
<IssuesIcon
|
||||||
v-if="customServerConfig.ramInGb < 4"
|
v-if="customServerConfig.ramInGb < 4"
|
||||||
v-tooltip="'This might not be enough resources for your Minecraft server.'"
|
v-tooltip="'This might not be enough resources for your Minecraft server.'"
|
||||||
class="h-6 w-6 text-orange"
|
class="h-6 w-6 text-orange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<p v-if="existingPlan" class="mt-1 mb-2 text-secondary">
|
||||||
|
Your current plan has <strong>{{ existingPlan.metadata.ram / 1024 }} GB RAM</strong> and
|
||||||
|
<strong>{{ existingPlan.metadata.cpu }} vCPUs</strong>.
|
||||||
|
</p>
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<div class="flex w-full gap-2 items-center">
|
<div class="flex w-full gap-2 items-center">
|
||||||
<Slider
|
<Slider
|
||||||
@@ -125,7 +127,7 @@
|
|||||||
<div class="font-semibold text-nowrap"></div>
|
<div class="font-semibold text-nowrap"></div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="customMatchingProduct && !customOutOfStock"
|
v-if="customMatchingProduct && (existingPlan || !customOutOfStock)"
|
||||||
class="flex sm:flex-row flex-col gap-4 w-full"
|
class="flex sm:flex-row flex-col gap-4 w-full"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col w-full gap-2">
|
<div class="flex flex-col w-full gap-2">
|
||||||
@@ -166,6 +168,16 @@
|
|||||||
>
|
>
|
||||||
<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">
|
||||||
|
<p class="m-0">
|
||||||
|
The prices below reflect the new <strong>renewal cost</strong> of your upgraded
|
||||||
|
subscription.
|
||||||
|
</p>
|
||||||
|
<p class="m-0">
|
||||||
|
Today, you will be charged a prorated amount for the remainder of your current billing
|
||||||
|
cycle.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<div
|
<div
|
||||||
v-for="([interval, rawPrice], index) in Object.entries(price.prices.intervals)"
|
v-for="([interval, rawPrice], index) in Object.entries(price.prices.intervals)"
|
||||||
@@ -228,7 +240,10 @@
|
|||||||
v-if="purchaseModalStep === (mutatedProduct.metadata.type === 'pyro' && !projectId ? 3 : 2)"
|
v-if="purchaseModalStep === (mutatedProduct.metadata.type === 'pyro' && !projectId ? 3 : 2)"
|
||||||
class="md:w-[650px]"
|
class="md:w-[650px]"
|
||||||
>
|
>
|
||||||
<div v-if="mutatedProduct.metadata.type === 'pyro'" class="r-4 rounded-xl bg-bg p-4 mb-4">
|
<div
|
||||||
|
v-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>
|
<p class="my-2 text-lg font-bold text-primary">Server details</p>
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<img
|
<img
|
||||||
@@ -248,12 +263,19 @@
|
|||||||
<div class="r-4 rounded-xl bg-bg p-4">
|
<div class="r-4 rounded-xl bg-bg p-4">
|
||||||
<p class="my-2 text-lg font-bold text-primary">Purchase details</p>
|
<p class="my-2 text-lg font-bold text-primary">Purchase details</p>
|
||||||
<div class="mb-2 flex justify-between">
|
<div class="mb-2 flex justify-between">
|
||||||
<span class="text-secondary"
|
<span class="text-secondary">
|
||||||
>{{ mutatedProduct.metadata.type === 'midas' ? 'Modrinth+' : 'Modrinth Servers' }}
|
{{ mutatedProduct.metadata.type === 'midas' ? 'Modrinth+' : 'Modrinth Servers' }}
|
||||||
{{ selectedPlan }}</span
|
{{
|
||||||
>
|
existingPlan
|
||||||
<span class="text-secondary text-end">
|
? `(${dayjs(renewalDate).diff(dayjs(), 'days')} days prorated)`
|
||||||
{{ formatPrice(locale, price.prices.intervals[selectedPlan], price.currency_code) }} /
|
: selectedPlan
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
<span v-if="existingPlan" class="text-secondary text-end">
|
||||||
|
{{ formatPrice(locale, total - tax, price.currency_code) }}
|
||||||
|
</span>
|
||||||
|
<span v-else class="text-secondary text-end">
|
||||||
|
{{ formatPrice(locale, total - tax, price.currency_code) }} /
|
||||||
{{ selectedPlan }}
|
{{ selectedPlan }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -266,7 +288,7 @@
|
|||||||
<div class="mt-4 flex justify-between border-0 border-t border-solid border-code-bg pt-4">
|
<div class="mt-4 flex justify-between border-0 border-t border-solid border-code-bg pt-4">
|
||||||
<span class="text-lg font-bold">Today's total</span>
|
<span class="text-lg font-bold">Today's total</span>
|
||||||
<span class="text-lg font-extrabold text-primary text-end">
|
<span class="text-lg font-extrabold text-primary text-end">
|
||||||
{{ formatPrice(locale, price.prices.intervals[selectedPlan], price.currency_code) }}
|
{{ formatPrice(locale, total, price.currency_code) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -363,7 +385,8 @@
|
|||||||
<br />
|
<br />
|
||||||
You'll be charged
|
You'll be charged
|
||||||
{{ formatPrice(locale, price.prices.intervals[selectedPlan], price.currency_code) }} /
|
{{ formatPrice(locale, price.prices.intervals[selectedPlan], price.currency_code) }} /
|
||||||
{{ selectedPlan }} plus applicable taxes starting today, until you cancel.
|
{{ selectedPlan }} plus applicable taxes starting
|
||||||
|
{{ existingPlan ? dayjs(renewalDate).format('MMMM D, YYYY') : 'today' }}, until you cancel.
|
||||||
<br />
|
<br />
|
||||||
You can cancel anytime from your settings page.
|
You can cancel anytime from your settings page.
|
||||||
</p>
|
</p>
|
||||||
@@ -389,12 +412,19 @@
|
|||||||
:disabled="
|
:disabled="
|
||||||
paymentLoading ||
|
paymentLoading ||
|
||||||
(mutatedProduct.metadata.type === 'pyro' && !projectId && !serverName) ||
|
(mutatedProduct.metadata.type === 'pyro' && !projectId && !serverName) ||
|
||||||
customAllowedToContinue
|
customNotAllowedToContinue ||
|
||||||
|
upgradeNotAllowedToContinue
|
||||||
"
|
"
|
||||||
@click="nextStep"
|
@click="nextStep"
|
||||||
>
|
>
|
||||||
<RightArrowIcon />
|
<template v-if="customServer && customLoading">
|
||||||
{{ mutatedProduct.metadata.type === 'pyro' && !projectId ? 'Next' : 'Select' }}
|
<SpinnerIcon class="animate-spin" />
|
||||||
|
Checking availability...
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<RightArrowIcon />
|
||||||
|
{{ mutatedProduct.metadata.type === 'pyro' && !projectId ? 'Next' : 'Select' }}
|
||||||
|
</template>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
<template
|
<template
|
||||||
@@ -468,6 +498,7 @@
|
|||||||
import { ref, computed, nextTick, reactive, watch } from 'vue'
|
import { ref, computed, nextTick, reactive, watch } from 'vue'
|
||||||
import NewModal from '../modal/NewModal.vue'
|
import NewModal from '../modal/NewModal.vue'
|
||||||
import {
|
import {
|
||||||
|
SpinnerIcon,
|
||||||
CardIcon,
|
CardIcon,
|
||||||
CheckCircleIcon,
|
CheckCircleIcon,
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
@@ -487,6 +518,7 @@ import { useVIntl, defineMessages } from '@vintl/vintl'
|
|||||||
import { Multiselect } from 'vue-multiselect'
|
import { Multiselect } from 'vue-multiselect'
|
||||||
import Checkbox from '../base/Checkbox.vue'
|
import Checkbox from '../base/Checkbox.vue'
|
||||||
import Slider from '../base/Slider.vue'
|
import Slider from '../base/Slider.vue'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
import Admonition from '../base/Admonition.vue'
|
import Admonition from '../base/Admonition.vue'
|
||||||
|
|
||||||
const { locale, formatMessage } = useVIntl()
|
const { locale, formatMessage } = useVIntl()
|
||||||
@@ -562,8 +594,25 @@ const props = defineProps({
|
|||||||
required: false,
|
required: false,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
existingSubscription: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
existingPlan: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
renewalDate: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const productType = computed(() => (props.customServer ? 'pyro' : props.product.metadata.type))
|
||||||
|
|
||||||
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',
|
||||||
@@ -645,7 +694,7 @@ const total = ref()
|
|||||||
|
|
||||||
const serverName = ref(props.serverName || '')
|
const serverName = ref(props.serverName || '')
|
||||||
const serverLoader = ref('Vanilla')
|
const serverLoader = ref('Vanilla')
|
||||||
const eulaAccepted = ref(false)
|
const eulaAccepted = ref(!!props.existingSubscription)
|
||||||
|
|
||||||
const mutatedProduct = ref({ ...props.product })
|
const mutatedProduct = ref({ ...props.product })
|
||||||
const customMinRam = ref(0)
|
const customMinRam = ref(0)
|
||||||
@@ -653,11 +702,15 @@ const customMaxRam = ref(0)
|
|||||||
const customMatchingProduct = ref()
|
const customMatchingProduct = ref()
|
||||||
const customOutOfStock = ref(false)
|
const customOutOfStock = ref(false)
|
||||||
const customLoading = ref(true)
|
const customLoading = ref(true)
|
||||||
const customAllowedToContinue = computed(
|
const customNotAllowedToContinue = computed(
|
||||||
() =>
|
() =>
|
||||||
props.customServer &&
|
props.customServer &&
|
||||||
|
!props.existingSubscription &&
|
||||||
(!customMatchingProduct.value || customLoading.value || customOutOfStock.value),
|
(!customMatchingProduct.value || customLoading.value || customOutOfStock.value),
|
||||||
)
|
)
|
||||||
|
const upgradeNotAllowedToContinue = computed(
|
||||||
|
() => props.existingSubscription && (!hasUpgradeCapacityForConfig.value || customLoading.value),
|
||||||
|
)
|
||||||
|
|
||||||
const customServerConfig = reactive({
|
const customServerConfig = reactive({
|
||||||
ramInGb: 4,
|
ramInGb: 4,
|
||||||
@@ -670,7 +723,9 @@ const updateCustomServerProduct = () => {
|
|||||||
(product) => product.metadata.ram === customServerConfig.ram,
|
(product) => product.metadata.ram === customServerConfig.ram,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (customMatchingProduct.value) mutatedProduct.value = { ...customMatchingProduct.value }
|
if (customMatchingProduct.value) {
|
||||||
|
mutatedProduct.value = { ...customMatchingProduct.value }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let updateCustomServerStockTimeout = null
|
let updateCustomServerStockTimeout = null
|
||||||
@@ -688,19 +743,25 @@ const updateCustomServerStock = async () => {
|
|||||||
} else {
|
} else {
|
||||||
customOutOfStock.value = false
|
customOutOfStock.value = false
|
||||||
}
|
}
|
||||||
customLoading.value = false
|
} else if (!props.existingServer) {
|
||||||
} else {
|
|
||||||
console.error('No fetchCapacityStatuses function provided.')
|
console.error('No fetchCapacityStatuses function provided.')
|
||||||
customOutOfStock.value = true
|
customOutOfStock.value = true
|
||||||
}
|
}
|
||||||
|
customLoading.value = false
|
||||||
}, 300)
|
}, 300)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.customServer) {
|
function updateRamValues() {
|
||||||
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)
|
||||||
|
|
||||||
|
customServerConfig.ramInGb = customMinRam.value
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.customServer) {
|
||||||
|
updateRamValues()
|
||||||
|
|
||||||
const updateProductAndStock = () => {
|
const updateProductAndStock = () => {
|
||||||
updateCustomServerProduct()
|
updateCustomServerProduct()
|
||||||
updateCustomServerStock()
|
updateCustomServerStock()
|
||||||
@@ -880,16 +941,25 @@ async function refreshPayment(confirmationId, paymentMethodId) {
|
|||||||
id: paymentMethodId,
|
id: paymentMethodId,
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await props.sendBillingRequest({
|
const result = await props.sendBillingRequest(
|
||||||
charge: {
|
props.existingSubscription
|
||||||
type: 'new',
|
? {
|
||||||
product_id: mutatedProduct.value.id,
|
interval: selectedPlan.value,
|
||||||
interval: selectedPlan.value,
|
cancelled: false,
|
||||||
},
|
product: mutatedProduct.value.id,
|
||||||
existing_payment_intent: paymentIntentId.value,
|
payment_method: paymentMethodId,
|
||||||
metadata: metadata.value,
|
}
|
||||||
...base,
|
: {
|
||||||
})
|
charge: {
|
||||||
|
type: 'new',
|
||||||
|
product_id: mutatedProduct.value.id,
|
||||||
|
interval: selectedPlan.value,
|
||||||
|
},
|
||||||
|
existing_payment_intent: paymentIntentId.value,
|
||||||
|
metadata: metadata.value,
|
||||||
|
...base,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
if (!paymentIntentId.value) {
|
if (!paymentIntentId.value) {
|
||||||
paymentIntentId.value = result.payment_intent_id
|
paymentIntentId.value = result.payment_intent_id
|
||||||
@@ -903,10 +973,14 @@ async function refreshPayment(confirmationId, paymentMethodId) {
|
|||||||
|
|
||||||
if (confirmationId) {
|
if (confirmationId) {
|
||||||
confirmationToken.value = confirmationId
|
confirmationToken.value = confirmationId
|
||||||
inputtedPaymentMethod.value = result.payment_method
|
if (result.payment_method) {
|
||||||
|
inputtedPaymentMethod.value = result.payment_method
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedPaymentMethod.value = result.payment_method
|
if (result.payment_method) {
|
||||||
|
selectedPaymentMethod.value = result.payment_method
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
props.onError(err)
|
props.onError(err)
|
||||||
}
|
}
|
||||||
@@ -928,11 +1002,20 @@ async function submitPayment() {
|
|||||||
paymentLoading.value = false
|
paymentLoading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasUpgradeCapacityForConfig = computed(() => {
|
||||||
|
// TODO: Check for upgrade capacity here when Pyro provides route
|
||||||
|
return props.existingPlan
|
||||||
|
})
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
show: () => {
|
show: () => {
|
||||||
|
if (props.customServer) {
|
||||||
|
updateRamValues()
|
||||||
|
}
|
||||||
|
|
||||||
stripe = Stripe(props.publishableKey)
|
stripe = Stripe(props.publishableKey)
|
||||||
|
|
||||||
selectedPlan.value = 'yearly'
|
selectedPlan.value = props.existingSubscription ? props.existingSubscription.interval : 'yearly'
|
||||||
serverName.value = props.serverName || ''
|
serverName.value = props.serverName || ''
|
||||||
serverLoader.value = 'Vanilla'
|
serverLoader.value = 'Vanilla'
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user