Update billing with backend changes (#2505)

This commit is contained in:
Geometrically
2024-10-09 21:11:49 -07:00
committed by GitHub
parent d302795512
commit 570a4096f9
3 changed files with 72 additions and 49 deletions

View File

@@ -17,33 +17,24 @@
<span class="font-bold text-primary">
<template v-if="charge.product.metadata.type === 'midas'"> Modrinth Plus </template>
<template v-else> Unknown product </template>
<template v-if="charge.metadata.modrinth_subscription_interval">
{{ charge.metadata.modrinth_subscription_interval }}
<template v-if="charge.subscription_interval">
{{ charge.subscription_interval }}
</template>
</span>
<span>{{ formatPrice(charge.amount, charge.currency) }}</span>
<span>{{ formatPrice(charge.amount, charge.currency_code) }}</span>
</div>
<div class="flex items-center gap-1">
<Badge :color="charge.status === 'succeeded' ? 'green' : 'red'" :type="charge.status" />
{{ $dayjs.unix(charge.created).format("YYYY-MM-DD") }}
{{ $dayjs(charge.due).format("YYYY-MM-DD") }}
</div>
</div>
<a
v-if="charge.receipt_url"
class="iconified-button raised-button"
:href="charge.receipt_url"
>
<ReceiptTextIcon />
View receipt
</a>
</div>
</section>
</div>
</template>
<script setup>
import { ReceiptTextIcon } from "@modrinth/assets";
import { Breadcrumbs, Badge } from "@modrinth/ui";
import { products } from "~/generated/state.json";
@@ -58,15 +49,17 @@ const { data: charges } = await useAsyncData(
() => useBaseFetch("billing/payments", { internal: true }),
{
transform: (charges) => {
return charges.map((charge) => {
const product = products.find((product) =>
product.prices.some((price) => price.id === charge.metadata.modrinth_price_id),
);
return charges
.filter((charge) => charge.status !== "open" && charge.status !== "cancelled")
.map((charge) => {
const product = products.find((product) =>
product.prices.some((price) => price.id === charge.price_id),
);
charge.product = product;
charge.product = product;
return charge;
});
return charge;
});
},
},
);

View File

@@ -8,22 +8,20 @@
:title="formatMessage(cancelModalMessages.title)"
:description="formatMessage(cancelModalMessages.description)"
:proceed-label="formatMessage(cancelModalMessages.action)"
@proceed="cancelSubscription(cancelSubscriptionId)"
@proceed="cancelSubscription(cancelSubscriptionId, true)"
/>
<div class="flex flex-wrap justify-between gap-4">
<div class="flex flex-col gap-4">
<template v-if="midasSubscription">
<span v-if="midasSubscription.status === 'active'">
You're currently subscribed to:
</span>
<span v-else-if="midasSubscription.status === 'payment-processing'" class="text-orange">
<template v-if="midasCharge">
<span v-if="midasCharge.status === 'open'"> You're currently subscribed to: </span>
<span v-else-if="midasCharge.status === 'processing'" class="text-orange">
Your payment is being processed. Perks will activate once payment is complete.
</span>
<span v-else-if="midasSubscription.status === 'cancelled'">
<span v-else-if="midasCharge.status === 'cancelled'">
You've cancelled your subscription. <br />
You will retain your perks until the end of the current billing cycle.
</span>
<span v-else-if="midasSubscription.status === 'payment-failed'" class="text-red">
<span v-else-if="midasCharge.status === 'failed'" class="text-red">
Your subscription payment failed. Please update your payment method.
</span>
</template>
@@ -49,34 +47,31 @@
<div class="flex w-full flex-wrap justify-between gap-4 xl:w-auto xl:flex-col">
<div class="flex flex-col gap-1 xl:ml-auto xl:text-right">
<span class="text-2xl font-bold text-dark">
<template v-if="midasSubscription">
<template v-if="midasCharge">
{{
formatPrice(
vintl.locale,
midasSubscriptionPrice.prices.intervals[midasSubscription.interval],
midasSubscriptionPrice.prices.intervals[midasCharge.subscription_interval],
midasSubscriptionPrice.currency_code,
)
}}
/
{{ midasSubscription.interval }}
{{ midasCharge.subscription_interval }}
</template>
<template v-else>
{{ formatPrice(vintl.locale, price.prices.intervals.monthly, price.currency_code) }}
/ month
</template>
</span>
<template v-if="midasSubscription">
<template v-if="midasCharge">
<span class="text-sm text-secondary">
Since {{ $dayjs(midasSubscription.created).format("MMMM D, YYYY") }}
</span>
<span v-if="midasSubscription.status === 'active'" class="text-sm text-secondary">
Renews {{ $dayjs(midasSubscription.expires).format("MMMM D, YYYY") }}
<span v-if="midasCharge.status === 'open'" class="text-sm text-secondary">
Renews {{ $dayjs(midasCharge.due).format("MMMM D, YYYY") }}
</span>
<span
v-else-if="midasSubscription.status === 'cancelled'"
class="text-sm text-secondary"
>
Expires {{ $dayjs(midasSubscription.expires).format("MMMM D, YYYY") }}
<span v-else-if="midasCharge.status === 'cancelled'" class="text-sm text-secondary">
Expires {{ $dayjs(midasCharge.due).format("MMMM D, YYYY") }}
</span>
</template>
@@ -90,11 +85,11 @@
</span>
</div>
<div
v-if="midasSubscription && midasSubscription.status === 'payment-failed'"
v-if="midasCharge && midasCharge.status === 'failed'"
class="ml-auto flex flex-row-reverse items-center gap-2"
>
<button
v-if="midasSubscription && midasSubscription.status === 'payment-failed'"
v-if="midasCharge && midasCharge.status === 'failed'"
class="iconified-button raised-button"
@click="
() => {
@@ -123,7 +118,7 @@
</OverflowMenu>
</div>
<button
v-else-if="midasSubscription && midasSubscription.status !== 'cancelled'"
v-else-if="midasCharge && midasCharge.status !== 'cancelled'"
class="iconified-button raised-button !ml-auto"
@click="
() => {
@@ -134,6 +129,13 @@
>
<XIcon /> Cancel
</button>
<button
v-else-if="midasCharge && midasCharge.status === 'cancelled'"
class="btn btn-purple btn-large ml-auto"
@click="cancelSubscription(midasSubscription.id, false)"
>
<RightArrowIcon /> Resubscribe
</button>
<button
v-else
class="btn btn-purple btn-large ml-auto"
@@ -474,12 +476,14 @@ function loadStripe() {
const [
{ data: paymentMethods, refresh: refreshPaymentMethods },
{ data: charges, refresh: refreshCharges },
{ data: customer, refresh: refreshCustomer },
{ data: subscriptions, refresh: refreshSubscriptions },
] = await Promise.all([
useAsyncData("billing/payment_methods", () =>
useBaseFetch("billing/payment_methods", { internal: true }),
),
useAsyncData("billing/payments", () => useBaseFetch("billing/payments", { internal: true })),
useAsyncData("billing/customer", () => useBaseFetch("billing/customer", { internal: true })),
useAsyncData("billing/subscriptions", () =>
useBaseFetch("billing/subscriptions", { internal: true }),
@@ -487,18 +491,30 @@ const [
]);
async function refresh() {
await Promise.all([refreshPaymentMethods(), refreshCustomer(), refreshSubscriptions()]);
await Promise.all([
refreshPaymentMethods(),
refreshCharges(),
refreshCustomer(),
refreshSubscriptions(),
]);
}
const midasProduct = ref(products.find((x) => x.metadata.type === "midas"));
const midasSubscription = computed(() =>
subscriptions.value.find((x) => midasProduct.value.prices.find((y) => y.id === x.price_id)),
subscriptions.value.find(
(x) => x.status === "provisioned" && midasProduct.value.prices.find((y) => y.id === x.price_id),
),
);
const midasSubscriptionPrice = computed(() =>
midasSubscription.value
? midasProduct.value.prices.find((x) => x.id === midasSubscription.value.price_id)
: null,
);
const midasCharge = computed(() =>
midasSubscription.value
? charges.value.find((x) => x.subscription_id === midasSubscription.value.id)
: null,
);
const purchaseModal = ref();
const country = useUserCountry();
@@ -524,10 +540,18 @@ if (route.query.priceId && route.query.plan && route.query.redirect_status) {
price_id: route.query.priceId,
interval: route.query.plan,
created: Date.now(),
expires: route.query.plan === "yearly" ? Date.now() + 31536000000 : Date.now() + 2629746000,
status,
});
charges.value.push({
id: "temp",
price_id: route.query.priceId,
subscription_id: "temp",
status: "open",
due: Date.now() + (route.query.plan === "yearly" ? 31536000000 : 2629746000),
subscription_interval: route.query.plan,
});
await router.replace({ query: {} });
}
@@ -655,12 +679,15 @@ async function removePaymentMethod(index) {
}
const cancelSubscriptionId = ref();
async function cancelSubscription(id) {
async function cancelSubscription(id, cancelled) {
startLoading();
try {
await useBaseFetch(`billing/subscription/${id}`, {
internal: true,
method: "DELETE",
method: "PATCH",
body: {
cancelled,
},
});
await refresh();
} catch (err) {

View File

@@ -540,8 +540,11 @@ async function refreshPayment(confirmationId, paymentMethodId) {
}
const result = await props.sendBillingRequest({
product_id: props.product.id,
interval: selectedPlan.value,
charge: {
type: 'new',
product_id: props.product.id,
interval: selectedPlan.value,
},
existing_payment_intent: paymentIntentId.value,
...base,
})