feat: start of cross platform page system (#4731)

* feat: abstract api-client DI into ui package

* feat: cross platform page system

* feat: tanstack as cross platform useAsyncData

* feat: archon servers routes + labrinth billing routes

* fix: dont use partial

* feat: migrate server list page to tanstack + api-client + re-enabled broken features!

* feat: migrate servers manage page to api-client before page system

* feat: migrate manage page to page system

* fix: type issues

* fix: upgrade wrapper bugs

* refactor: move state types into api-client

* feat: disable financial stuff on app frontend

* feat: finalize cross platform page system for now

* fix: lint

* fix: build issues

* feat: remove papaparse

* fix: lint

* fix: interface error

---------

Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
This commit is contained in:
Calum H.
2025-11-14 17:15:09 +00:00
committed by GitHub
parent 26feaf753a
commit 7ccc32675b
79 changed files with 2631 additions and 1259 deletions

View File

@@ -1,19 +1,11 @@
import type { Labrinth } from '@modrinth/api-client'
import { loadStripe, type Stripe as StripeJs, type StripeElements } from '@stripe/stripe-js'
import type { ContactOption } from '@stripe/stripe-js/dist/stripe-js/elements/address'
import type Stripe from 'stripe'
import { computed, type Ref, ref } from 'vue'
import type {
BasePaymentIntentResponse,
ChargeRequestType,
CreatePaymentIntentRequest,
CreatePaymentIntentResponse,
PaymentRequestType,
ServerBillingInterval,
ServerPlan,
UpdatePaymentIntentRequest,
UpdatePaymentIntentResponse,
} from '../utils/billing.ts'
import type { ServerBillingInterval } from '../components/billing/ModrinthServersPurchaseModal.vue'
import { getPriceForInterval } from '../utils/product-utils'
// export type CreateElements = (
// paymentMethods: Stripe.PaymentMethod[],
@@ -29,13 +21,17 @@ export const useStripe = (
customer: Stripe.Customer,
paymentMethods: Stripe.PaymentMethod[],
currency: string,
product: Ref<ServerPlan | undefined>,
product: Ref<Labrinth.Billing.Internal.Product | undefined>,
interval: Ref<ServerBillingInterval>,
region: Ref<string | undefined>,
project: Ref<string | undefined>,
initiatePayment: (
body: CreatePaymentIntentRequest | UpdatePaymentIntentRequest,
) => Promise<CreatePaymentIntentResponse | UpdatePaymentIntentResponse | null>,
body: Labrinth.Billing.Internal.InitiatePaymentRequest,
) => Promise<
| Labrinth.Billing.Internal.InitiatePaymentResponse
| Labrinth.Billing.Internal.EditSubscriptionResponse
| null
>,
onError: (err: Error) => void,
affiliateCode?: Ref<string | null>,
) => {
@@ -62,18 +58,6 @@ export const useStripe = (
stripe.value = await loadStripe(publishableKey)
}
function createIntent(
body: CreatePaymentIntentRequest,
): Promise<CreatePaymentIntentResponse | null> {
return initiatePayment(body) as Promise<CreatePaymentIntentResponse | null>
}
function updateIntent(
body: UpdatePaymentIntentRequest,
): Promise<UpdatePaymentIntentResponse | null> {
return initiatePayment(body) as Promise<UpdatePaymentIntentResponse | null>
}
const planPrices = computed(() => {
return product.value?.prices.find((x) => x.currency_code === currency)
})
@@ -180,9 +164,9 @@ export const useStripe = (
} = createElements({
mode: 'payment',
currency: currency.toLowerCase(),
amount: product.value?.prices.find((x) => x.currency_code === currency)?.prices.intervals[
interval.value
],
amount: product.value
? getPriceForInterval(product.value, currency, interval.value)
: undefined,
paymentMethodCreation: 'manual',
setupFutureUsage: 'off_session',
})
@@ -208,82 +192,61 @@ export const useStripe = (
selectedPaymentMethod.value = paymentMethods.find((x) => x.id === id)
}
const requestType: PaymentRequestType = confirmation
? {
type: 'confirmation_token',
token: id,
}
: {
type: 'payment_method',
id: id,
}
if (!product.value) {
return handlePaymentError('No product selected')
}
const charge: ChargeRequestType = {
type: 'new',
product_id: product.value?.id,
interval: interval.value,
const request: Labrinth.Billing.Internal.InitiatePaymentRequest = {
type: confirmation ? 'confirmation_token' : 'payment_method',
...(confirmation ? { token: id } : { id }),
charge: {
type: 'new',
product_id: product.value.id,
interval: interval.value as Labrinth.Billing.Internal.PriceDuration,
},
...(paymentIntentId.value ? { existing_payment_intent: paymentIntentId.value } : {}),
metadata: {
type: 'pyro',
server_region: region.value,
source: project.value
? {
project_id: project.value,
}
: {},
...(affiliateCode?.value ? { affiliate_code: affiliateCode.value } : {}),
},
}
let result: BasePaymentIntentResponse | null = null
const affiliateMetadata =
affiliateCode && affiliateCode.value
? {
affiliate_code: affiliateCode.value,
}
: {}
const metadata: CreatePaymentIntentRequest['metadata'] = {
type: 'pyro',
server_region: region.value,
source: project.value
? {
project_id: project.value,
}
: {},
...affiliateMetadata,
}
if (paymentIntentId.value) {
result = await updateIntent({
...requestType,
charge,
existing_payment_intent: paymentIntentId.value,
metadata,
})
if (result) console.log(`Updated payment intent: ${interval.value} for ${result.total}`)
} else {
const created = await createIntent({
...requestType,
charge,
metadata: metadata,
})
if (created) {
paymentIntentId.value = created.payment_intent_id
clientSecret.value = created.client_secret
result = created
console.log(`Created payment intent: ${interval.value} for ${created.total}`)
}
}
const result = await initiatePayment(request)
if (!result) {
tax.value = 0
total.value = 0
noPaymentRequired.value = true
} else {
if (result.payment_intent_id) {
paymentIntentId.value = result.payment_intent_id
}
if (result.client_secret) {
clientSecret.value = result.client_secret
}
tax.value = result.tax
total.value = result.total
noPaymentRequired.value = false
console.log(
`${paymentIntentId.value ? 'Updated' : 'Created'} payment intent: ${interval.value} for ${result.total}`,
)
}
if (confirmation) {
confirmationToken.value = id
if (result && result.payment_method) {
inputtedPaymentMethod.value = result.payment_method
if (result && 'payment_method' in result && result.payment_method) {
// payment_method is a string ID from the API, need to find the full object
const method = paymentMethods.find((x) => x.id === result.payment_method)
if (method) {
inputtedPaymentMethod.value = method
}
}
}
} catch (err) {
@@ -384,11 +347,12 @@ export const useStripe = (
}
submittingPayment.value = true
const productPrice = product.value?.prices.find((x) => x.currency_code === currency)
const { error } = await stripe.value.confirmPayment({
clientSecret: secert,
confirmParams: {
confirmation_token: confirmationToken.value,
return_url: `${returnUrl}?priceId=${product.value?.prices.find((x) => x.currency_code === currency)?.id}&plan=${interval.value}`,
return_url: `${returnUrl}?priceId=${productPrice?.id}&plan=${interval.value}`,
},
})