You've already forked AstralRinth
forked from didirus/AstralRinth
Fixes to billing
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
RightArrowIcon,
|
||||
XIcon,
|
||||
CheckCircleIcon,
|
||||
SpinnerIcon,
|
||||
} from '@modrinth/assets'
|
||||
import type {
|
||||
CreatePaymentIntentRequest,
|
||||
@@ -27,6 +28,7 @@ import RegionSelector from './ServersPurchase1Region.vue'
|
||||
import PaymentMethodSelector from './ServersPurchase2PaymentMethod.vue'
|
||||
import ConfirmPurchase from './ServersPurchase3Review.vue'
|
||||
import { useStripe } from '../../composables/stripe'
|
||||
import ModalLoadingIndicator from '../modal/ModalLoadingIndicator.vue'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
@@ -49,11 +51,12 @@ const props = defineProps<{
|
||||
initiatePayment: (
|
||||
body: CreatePaymentIntentRequest | UpdatePaymentIntentRequest,
|
||||
) => Promise<UpdatePaymentIntentResponse | CreatePaymentIntentResponse>
|
||||
onError: (err: Error) => void
|
||||
}>()
|
||||
|
||||
const modal = useTemplateRef<InstanceType<typeof NewModal>>('modal')
|
||||
const selectedPlan = ref<ServerPlan>()
|
||||
const selectedInterval = ref<ServerBillingInterval>()
|
||||
const selectedInterval = ref<ServerBillingInterval>('quarterly')
|
||||
const loading = ref(false)
|
||||
|
||||
const {
|
||||
@@ -72,21 +75,22 @@ const {
|
||||
reloadPaymentIntent,
|
||||
hasPaymentMethod,
|
||||
submitPayment,
|
||||
completingPurchase,
|
||||
} = useStripe(
|
||||
props.publishableKey,
|
||||
props.customer,
|
||||
props.paymentMethods,
|
||||
props.clientSecret,
|
||||
props.currency,
|
||||
selectedPlan,
|
||||
selectedInterval,
|
||||
props.initiatePayment,
|
||||
console.error,
|
||||
props.onError,
|
||||
)
|
||||
|
||||
const selectedRegion = ref<string>()
|
||||
const customServer = ref<boolean>(false)
|
||||
const acceptedEula = ref<boolean>(false)
|
||||
const firstTimeThru = ref<boolean>(true)
|
||||
|
||||
type Step = 'region' | 'payment' | 'review'
|
||||
|
||||
@@ -111,9 +115,13 @@ const currentPing = computed(() => {
|
||||
|
||||
const currentStep = ref<Step>()
|
||||
|
||||
const currentStepIndex = computed(() => steps.indexOf(currentStep.value))
|
||||
const previousStep = computed(() => steps[steps.indexOf(currentStep.value) - 1])
|
||||
const nextStep = computed(() => steps[steps.indexOf(currentStep.value) + 1])
|
||||
const currentStepIndex = computed(() => (currentStep.value ? steps.indexOf(currentStep.value) : -1))
|
||||
const previousStep = computed(() =>
|
||||
currentStep.value ? steps[steps.indexOf(currentStep.value) - 1] : undefined,
|
||||
)
|
||||
const nextStep = computed(() =>
|
||||
currentStep.value ? steps[steps.indexOf(currentStep.value) + 1] : undefined,
|
||||
)
|
||||
|
||||
const canProceed = computed(() => {
|
||||
switch (currentStep.value) {
|
||||
@@ -122,7 +130,7 @@ const canProceed = computed(() => {
|
||||
case 'payment':
|
||||
return selectedPaymentMethod.value || !loadingElements.value
|
||||
case 'review':
|
||||
return acceptedEula.value && hasPaymentMethod.value
|
||||
return acceptedEula.value && hasPaymentMethod.value && !completingPurchase.value
|
||||
default:
|
||||
return false
|
||||
}
|
||||
@@ -135,13 +143,14 @@ async function beforeProceed(step: string) {
|
||||
case 'payment':
|
||||
await initializeStripe()
|
||||
|
||||
if (primaryPaymentMethodId.value) {
|
||||
if (primaryPaymentMethodId.value && firstTimeThru.value) {
|
||||
const paymentMethod = await props.paymentMethods.find(
|
||||
(x) => x.id === primaryPaymentMethodId.value,
|
||||
)
|
||||
await selectPaymentMethod(paymentMethod)
|
||||
await setStep('review', true)
|
||||
return true
|
||||
firstTimeThru.value = false
|
||||
return false
|
||||
}
|
||||
return true
|
||||
case 'review':
|
||||
@@ -166,13 +175,13 @@ async function afterProceed(step: string) {
|
||||
}
|
||||
}
|
||||
|
||||
async function setStep(step: Step, skipValidation = false) {
|
||||
async function setStep(step: Step | undefined, skipValidation = false) {
|
||||
if (!step) {
|
||||
await submitPayment(props.returnUrl)
|
||||
return
|
||||
}
|
||||
|
||||
if (!canProceed.value || skipValidation) {
|
||||
if (!skipValidation && !canProceed.value) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -191,6 +200,7 @@ function begin(interval: ServerBillingInterval, plan?: ServerPlan) {
|
||||
customServer.value = !selectedPlan.value
|
||||
selectedPaymentMethod.value = undefined
|
||||
currentStep.value = steps[0]
|
||||
firstTimeThru.value = true
|
||||
modal.value?.show()
|
||||
}
|
||||
|
||||
@@ -206,7 +216,7 @@ defineExpose({
|
||||
<button
|
||||
v-if="index < currentStepIndex"
|
||||
class="bg-transparent active:scale-95 font-bold text-secondary p-0"
|
||||
@click="setStep(id)"
|
||||
@click="setStep(id, true)"
|
||||
>
|
||||
{{ formatMessage(title) }}
|
||||
</button>
|
||||
@@ -249,31 +259,48 @@ defineExpose({
|
||||
v-else-if="
|
||||
currentStep === 'review' &&
|
||||
hasPaymentMethod &&
|
||||
selectedRegion &&
|
||||
currentRegion &&
|
||||
selectedInterval &&
|
||||
selectedPlan
|
||||
"
|
||||
ref="currentStepRef"
|
||||
v-model:interval="selectedInterval"
|
||||
v-model:accepted-eula="acceptedEula"
|
||||
:currency="currency"
|
||||
:plan="selectedPlan"
|
||||
:region="regions.find((x) => x.shortcode === selectedRegion)"
|
||||
:region="currentRegion"
|
||||
:ping="currentPing"
|
||||
:loading="paymentMethodLoading"
|
||||
:selected-payment-method="selectedPaymentMethod || inputtedPaymentMethod"
|
||||
:tax="tax"
|
||||
:total="total"
|
||||
:on-error="console.error"
|
||||
@change-payment-method="setStep('payment')"
|
||||
@change-payment-method="setStep('payment', true)"
|
||||
@reload-payment-intent="reloadPaymentIntent"
|
||||
@error="console.error"
|
||||
/>
|
||||
<div v-else>Something went wrong</div>
|
||||
<div
|
||||
v-show="
|
||||
selectedPaymentMethod === undefined &&
|
||||
currentStep === 'payment' &&
|
||||
selectedPlan &&
|
||||
selectedInterval
|
||||
"
|
||||
class="min-h-[16rem] flex flex-col gap-2 mt-2 p-4 bg-table-alternateRow rounded-xl justify-center items-center"
|
||||
>
|
||||
<div v-show="loadingElements">
|
||||
<ModalLoadingIndicator :error="loadingElementsFailed">
|
||||
Loading...
|
||||
<template #error> Error loading Stripe payment UI. </template>
|
||||
</ModalLoadingIndicator>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<div id="address-element"></div>
|
||||
<div id="payment-element" class="mt-4"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2 justify-between mt-4">
|
||||
<ButtonStyled>
|
||||
<button v-if="previousStep" @click="previousStep && setStep(previousStep)">
|
||||
<button v-if="previousStep" @click="previousStep && setStep(previousStep, true)">
|
||||
<LeftArrowIcon /> {{ formatMessage(commonMessages.backButton) }}
|
||||
</button>
|
||||
<button v-else @click="modal?.hide()">
|
||||
@@ -282,9 +309,10 @@ defineExpose({
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button :disabled="!canProceed" @click="setStep(nextStep)">
|
||||
<button v-tooltip="currentStep === 'review' && !acceptedEula ? 'You must accept the Minecraft EULA to proceed.' : undefined" :disabled="!canProceed" @click="setStep(nextStep)">
|
||||
<template v-if="currentStep === 'review'">
|
||||
<CheckCircleIcon />
|
||||
<SpinnerIcon v-if="completingPurchase" class="animate-spin" />
|
||||
<CheckCircleIcon v-else />
|
||||
Subscribe
|
||||
</template>
|
||||
<template v-else>
|
||||
|
||||
@@ -214,10 +214,10 @@
|
||||
{{ interval }}
|
||||
</span>
|
||||
<span
|
||||
v-if="interval === 'yearly'"
|
||||
v-if="interval === 'yearly' || interval === 'quarterly'"
|
||||
class="rounded-full bg-brand px-2 py-1 font-bold text-brand-inverted"
|
||||
>
|
||||
SAVE {{ calculateSavings(price.prices.intervals.monthly, rawPrice) }}%
|
||||
SAVE {{ calculateSavings(price.prices.intervals.monthly, rawPrice, interval === 'quarterly' ? 3 : 12) }}%
|
||||
</span>
|
||||
<span class="ml-auto text-lg" :class="{ 'text-secondary': selectedPlan !== interval }">
|
||||
{{ formatPrice(locale, rawPrice, price.currency_code) }}
|
||||
|
||||
@@ -51,19 +51,4 @@ const messages = defineMessages({
|
||||
@select="emit('select', undefined)"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-show="selected === undefined"
|
||||
class="min-h-[16rem] flex flex-col gap-2 mt-2 p-4 bg-table-alternateRow rounded-xl justify-center items-center"
|
||||
>
|
||||
<div v-show="loadingElements">
|
||||
<ModalLoadingIndicator :error="loadingElementsFailed">
|
||||
Loading...
|
||||
<template #error> Error loading Stripe payment UI. </template>
|
||||
</ModalLoadingIndicator>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<div id="address-element"></div>
|
||||
<div id="payment-element" class="mt-4"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -38,11 +38,10 @@ const props = defineProps<{
|
||||
ping?: number
|
||||
loading?: boolean
|
||||
selectedPaymentMethod: Stripe.PaymentMethod | undefined
|
||||
onError: (error: Error) => void
|
||||
}>()
|
||||
|
||||
const interval = defineModel<ServerBillingInterval>('interval')
|
||||
const acceptedEula = defineModel<boolean>('accepted-eula', { required: true })
|
||||
const interval = defineModel<ServerBillingInterval>('interval', { required: true })
|
||||
const acceptedEula = defineModel<boolean>('acceptedEula', { required: true })
|
||||
|
||||
const prices = computed(() => {
|
||||
return props.plan.prices.find((x) => x.currency_code === props.currency)
|
||||
@@ -143,14 +142,14 @@ function setInterval(newInterval: ServerBillingInterval) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div class="grid grid-cols-2 gap-2 mt-4">
|
||||
<button
|
||||
:class="
|
||||
interval === 'monthly'
|
||||
? 'bg-button-bg border-transparent'
|
||||
: 'bg-transparent border-button-border'
|
||||
"
|
||||
class="mt-4 rounded-2xl active:scale-[0.98] transition-transform duration-100 border-2 border-solid p-4 flex items-center gap-2"
|
||||
class="rounded-2xl active:scale-[0.98] transition-transform duration-100 border-2 border-solid p-4 flex items-center gap-2"
|
||||
@click="setInterval('monthly')"
|
||||
>
|
||||
<RadioButtonCheckedIcon v-if="interval === 'monthly'" class="size-6 text-brand" />
|
||||
@@ -167,27 +166,27 @@ function setInterval(newInterval: ServerBillingInterval) {
|
||||
</button>
|
||||
<button
|
||||
:class="
|
||||
interval === 'yearly'
|
||||
interval === 'quarterly'
|
||||
? 'bg-button-bg border-transparent'
|
||||
: 'bg-transparent border-button-border'
|
||||
"
|
||||
class="mt-4 rounded-2xl active:scale-[0.98] transition-transform duration-100 border-2 border-solid p-4 flex items-center gap-2"
|
||||
@click="setInterval('yearly')"
|
||||
class="rounded-2xl active:scale-[0.98] transition-transform duration-100 border-2 border-solid p-4 flex items-center gap-2"
|
||||
@click="setInterval('quarterly')"
|
||||
>
|
||||
<RadioButtonCheckedIcon v-if="interval === 'yearly'" class="size-6 text-brand" />
|
||||
<RadioButtonCheckedIcon v-if="interval === 'quarterly'" class="size-6 text-brand" />
|
||||
<RadioButtonIcon v-else class="size-6 text-secondary" />
|
||||
<div class="flex flex-col items-start gap-1 font-medium text-primary">
|
||||
<span class="flex items-center gap-1" :class="{ 'text-contrast': interval === 'yearly' }"
|
||||
>Pay yearly
|
||||
<span class="flex items-center gap-1" :class="{ 'text-contrast': interval === 'quarterly' }"
|
||||
>Pay quarterly
|
||||
<span class="text-xs font-bold text-brand px-1.5 py-0.5 rounded-full bg-brand-highlight"
|
||||
>{{ interval === 'quarterly' ? 'Saving' : 'Save' }} 16%</span
|
||||
>{{ interval === 'quarterly' ? 'Saving' : 'Save' }} 16%</span
|
||||
></span
|
||||
>
|
||||
<span class="text-sm text-secondary flex items-center gap-1"
|
||||
>{{
|
||||
>{{
|
||||
formatPrice(
|
||||
locale,
|
||||
prices?.prices?.intervals?.['yearly'] ?? 0 / monthsInInterval['yearly'],
|
||||
prices?.prices?.intervals?.['quarterly'] ?? 0 / monthsInInterval['quarterly'],
|
||||
currency,
|
||||
true,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user