mirror of
https://github.com/modrinth/code.git
synced 2026-09-05 06:19:11 +00:00
refactor guest plan select logic into provider
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Labrinth } from '@modrinth/api-client'
|
import type { Labrinth } from '@modrinth/api-client'
|
||||||
|
import { ChevronRightIcon } from '@modrinth/assets'
|
||||||
import { computed, ref, useTemplateRef } from 'vue'
|
import { computed, ref, useTemplateRef } from 'vue'
|
||||||
|
|
||||||
import { ChevronRightIcon } from '@modrinth/assets'
|
|
||||||
import NewModal from '../modal/NewModal.vue'
|
import NewModal from '../modal/NewModal.vue'
|
||||||
import type { ServerBillingInterval } from './ModrinthServersPurchaseModal.vue'
|
import type { ServerBillingInterval } from './ModrinthServersPurchaseModal.vue'
|
||||||
import PlanSelector from './ServersPurchase0Plan.vue'
|
import PlanSelector from './ServersPurchase0Plan.vue'
|
||||||
|
|||||||
@@ -217,6 +217,7 @@ import { useRoute, useRouter } from 'vue-router'
|
|||||||
|
|
||||||
import MedalServerListing from '#ui/components/servers/marketing/MedalServerListing.vue'
|
import MedalServerListing from '#ui/components/servers/marketing/MedalServerListing.vue'
|
||||||
import ServerListing from '#ui/components/servers/ServerListing.vue'
|
import ServerListing from '#ui/components/servers/ServerListing.vue'
|
||||||
|
import { createHostingPurchaseIntentContext, provideHostingPurchaseIntent } from '#ui/providers'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
stripePublishableKey: string
|
stripePublishableKey: string
|
||||||
@@ -231,17 +232,6 @@ const client = injectModrinthClient()
|
|||||||
const loggedIn = computed(() => !!auth.user.value)
|
const loggedIn = computed(() => !!auth.user.value)
|
||||||
|
|
||||||
const isNuxt = computed(() => client instanceof NuxtModrinthClient)
|
const isNuxt = computed(() => client instanceof NuxtModrinthClient)
|
||||||
const PURCHASE_INTENT_STORAGE_KEY = 'modrinth:servers-purchase-intent'
|
|
||||||
const PURCHASE_INTENT_SOURCE = 'hosting-manage'
|
|
||||||
|
|
||||||
type ServerBillingInterval = 'monthly' | 'quarterly' | 'yearly'
|
|
||||||
|
|
||||||
type PendingPurchaseIntent = {
|
|
||||||
interval: ServerBillingInterval
|
|
||||||
planId: string | null
|
|
||||||
source: string
|
|
||||||
ts: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const isPollingForNewServers = ref(false)
|
const isPollingForNewServers = ref(false)
|
||||||
const pollingState = ref({
|
const pollingState = ref({
|
||||||
@@ -255,9 +245,6 @@ const purchaseModal = ref<InstanceType<typeof ModrinthServersPurchaseModal> | nu
|
|||||||
const resubscribeModal = ref<InstanceType<typeof ResubscribeModal> | null>(null)
|
const resubscribeModal = ref<InstanceType<typeof ResubscribeModal> | null>(null)
|
||||||
const affiliateCode = ref<string | null>(null)
|
const affiliateCode = ref<string | null>(null)
|
||||||
const selectedCurrency = ref<string>('USD')
|
const selectedCurrency = ref<string>('USD')
|
||||||
const lastSelectedInterval = ref<ServerBillingInterval>('quarterly')
|
|
||||||
const lastSelectedPlanId = ref<string | null | undefined>(undefined)
|
|
||||||
const pendingResumeIntent = ref<PendingPurchaseIntent | null>(null)
|
|
||||||
const regionPings = ref<
|
const regionPings = ref<
|
||||||
{
|
{
|
||||||
region: string
|
region: string
|
||||||
@@ -277,40 +264,6 @@ const pyroProducts = computed(() => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
function readPendingPurchaseIntent(): PendingPurchaseIntent | null {
|
|
||||||
if (typeof window === 'undefined') return null
|
|
||||||
const rawIntent = window.sessionStorage.getItem(PURCHASE_INTENT_STORAGE_KEY)
|
|
||||||
if (!rawIntent) return null
|
|
||||||
|
|
||||||
try {
|
|
||||||
const parsedIntent = JSON.parse(rawIntent) as Partial<PendingPurchaseIntent>
|
|
||||||
if (
|
|
||||||
(parsedIntent.interval === 'monthly' ||
|
|
||||||
parsedIntent.interval === 'quarterly' ||
|
|
||||||
parsedIntent.interval === 'yearly') &&
|
|
||||||
(parsedIntent.planId === null || typeof parsedIntent.planId === 'string') &&
|
|
||||||
typeof parsedIntent.source === 'string' &&
|
|
||||||
typeof parsedIntent.ts === 'number'
|
|
||||||
) {
|
|
||||||
return parsedIntent as PendingPurchaseIntent
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
function writePendingPurchaseIntent(intent: PendingPurchaseIntent) {
|
|
||||||
if (typeof window === 'undefined') return
|
|
||||||
window.sessionStorage.setItem(PURCHASE_INTENT_STORAGE_KEY, JSON.stringify(intent))
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearPendingPurchaseIntent() {
|
|
||||||
if (typeof window === 'undefined') return
|
|
||||||
window.sessionStorage.removeItem(PURCHASE_INTENT_STORAGE_KEY)
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: customer,
|
data: customer,
|
||||||
refetch: refetchCustomer,
|
refetch: refetchCustomer,
|
||||||
@@ -488,6 +441,8 @@ const {
|
|||||||
const hasError = computed(() => loggedIn.value && !!fetchError.value)
|
const hasError = computed(() => loggedIn.value && !!fetchError.value)
|
||||||
|
|
||||||
const serverList = computed<Archon.Servers.v0.Server[]>(() => {
|
const serverList = computed<Archon.Servers.v0.Server[]>(() => {
|
||||||
|
return []
|
||||||
|
|
||||||
if (!serverResponse.value) return []
|
if (!serverResponse.value) return []
|
||||||
return serverResponse.value.servers
|
return serverResponse.value.servers
|
||||||
})
|
})
|
||||||
@@ -560,25 +515,6 @@ const canOpenPurchaseModal = computed(() => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
function openCheckoutFromIntent(intent: PendingPurchaseIntent): boolean {
|
|
||||||
console.log('Attempting to open checkout from intent', intent)
|
|
||||||
if (!purchaseModal.value || !canOpenPurchaseModal.value) return false
|
|
||||||
|
|
||||||
lastSelectedInterval.value = intent.interval
|
|
||||||
lastSelectedPlanId.value = intent.planId
|
|
||||||
const defaultPlan =
|
|
||||||
pyroProducts.value.find((p) => p?.metadata?.type === 'pyro' && p.metadata.ram === 6144) ??
|
|
||||||
pyroProducts.value.find((p) => p?.metadata?.type === 'pyro') ??
|
|
||||||
pyroProducts.value[0]
|
|
||||||
const selectedPlan =
|
|
||||||
intent.planId === null
|
|
||||||
? null
|
|
||||||
: (pyroProducts.value.find((product) => product.id === intent.planId) ?? defaultPlan)
|
|
||||||
|
|
||||||
purchaseModal.value.show(intent.interval, selectedPlan)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleError(err: unknown) {
|
function handleError(err: unknown) {
|
||||||
const error = err as Error & { data?: { description?: string } }
|
const error = err as Error & { data?: { description?: string } }
|
||||||
addNotification({
|
addNotification({
|
||||||
@@ -588,78 +524,30 @@ function handleError(err: unknown) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function openPurchaseModal() {
|
function handleSignIn() {
|
||||||
guestPlanModal.value?.show(lastSelectedInterval.value, lastSelectedPlanId.value)
|
void auth.requestSignIn('/hosting/manage')
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleGuestPlanContinue(payload: {
|
const hostingPurchaseIntent = createHostingPurchaseIntentContext({
|
||||||
interval: ServerBillingInterval
|
authRequestSignIn: auth.requestSignIn,
|
||||||
planId: string | null
|
signInRedirectPath: '/hosting/manage',
|
||||||
}) {
|
intentSource: 'hosting-manage',
|
||||||
const intent: PendingPurchaseIntent = {
|
loggedIn,
|
||||||
interval: payload.interval,
|
availableProducts: pyroProducts,
|
||||||
planId: payload.planId,
|
canOpenCheckout: canOpenPurchaseModal,
|
||||||
source: PURCHASE_INTENT_SOURCE,
|
guestPlanModal,
|
||||||
ts: Date.now(),
|
checkoutModal: purchaseModal,
|
||||||
}
|
onCheckoutPending: () => {
|
||||||
|
|
||||||
lastSelectedInterval.value = payload.interval
|
|
||||||
lastSelectedPlanId.value = payload.planId
|
|
||||||
|
|
||||||
if (!loggedIn.value) {
|
|
||||||
writePendingPurchaseIntent(intent)
|
|
||||||
void auth.requestSignIn('/hosting/manage')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
pendingResumeIntent.value = intent
|
|
||||||
if (openCheckoutFromIntent(intent)) {
|
|
||||||
pendingResumeIntent.value = null
|
|
||||||
} else {
|
|
||||||
addNotification({
|
addNotification({
|
||||||
title: 'Purchase unavailable',
|
title: 'Purchase unavailable',
|
||||||
text: 'Payment information is still loading. Opening checkout as soon as it is ready.',
|
text: 'Payment information is still loading. Opening checkout as soon as it is ready.',
|
||||||
type: 'info',
|
type: 'info',
|
||||||
})
|
})
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSignIn() {
|
|
||||||
void auth.requestSignIn('/hosting/manage')
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
loggedIn,
|
|
||||||
(isLoggedIn) => {
|
|
||||||
if (!isLoggedIn) return
|
|
||||||
|
|
||||||
const pendingIntent = readPendingPurchaseIntent()
|
|
||||||
if (!pendingIntent || pendingIntent.source !== PURCHASE_INTENT_SOURCE) return
|
|
||||||
|
|
||||||
clearPendingPurchaseIntent()
|
|
||||||
pendingResumeIntent.value = pendingIntent
|
|
||||||
lastSelectedInterval.value = pendingIntent.interval
|
|
||||||
lastSelectedPlanId.value = pendingIntent.planId
|
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
})
|
||||||
)
|
provideHostingPurchaseIntent(hostingPurchaseIntent)
|
||||||
|
|
||||||
watch(
|
const { openPurchaseModal, handleGuestPlanContinue } = hostingPurchaseIntent
|
||||||
() =>
|
|
||||||
[
|
|
||||||
loggedIn.value,
|
|
||||||
canOpenPurchaseModal.value,
|
|
||||||
pendingResumeIntent.value,
|
|
||||||
purchaseModal.value,
|
|
||||||
] as const,
|
|
||||||
([isLoggedIn, canOpen, pendingIntent]) => {
|
|
||||||
if (!isLoggedIn || !canOpen || !pendingIntent) return
|
|
||||||
if (openCheckoutFromIntent(pendingIntent)) {
|
|
||||||
pendingResumeIntent.value = null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true },
|
|
||||||
)
|
|
||||||
|
|
||||||
const { data: subscriptions } = useQuery({
|
const { data: subscriptions } = useQuery({
|
||||||
queryKey: ['billing', 'subscriptions'],
|
queryKey: ['billing', 'subscriptions'],
|
||||||
|
|||||||
@@ -0,0 +1,180 @@
|
|||||||
|
import type { Labrinth } from '@modrinth/api-client'
|
||||||
|
import { type Ref, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
import { createContext } from '.'
|
||||||
|
|
||||||
|
const PURCHASE_INTENT_STORAGE_KEY = 'modrinth:servers-purchase-intent'
|
||||||
|
|
||||||
|
export type ServerBillingInterval = 'monthly' | 'quarterly' | 'yearly'
|
||||||
|
|
||||||
|
export type PendingPurchaseIntent = {
|
||||||
|
interval: ServerBillingInterval
|
||||||
|
planId: string | null
|
||||||
|
source: string
|
||||||
|
ts: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GuestPlanContinuePayload = {
|
||||||
|
interval: ServerBillingInterval
|
||||||
|
planId: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CheckoutModalHandle = {
|
||||||
|
show: (interval: ServerBillingInterval, plan?: Labrinth.Billing.Internal.Product | null) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GuestPlanModalHandle = {
|
||||||
|
show: (initialInterval?: ServerBillingInterval, initialPlanId?: string | null) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HostingPurchaseIntentContext {
|
||||||
|
openPurchaseModal: () => void
|
||||||
|
handleGuestPlanContinue: (payload: GuestPlanContinuePayload) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateHostingPurchaseIntentContextOptions {
|
||||||
|
authRequestSignIn: (redirectPath: string) => void | Promise<void>
|
||||||
|
signInRedirectPath: string
|
||||||
|
intentSource: string
|
||||||
|
loggedIn: Readonly<Ref<boolean>>
|
||||||
|
availableProducts: Readonly<Ref<Labrinth.Billing.Internal.Product[]>>
|
||||||
|
canOpenCheckout: Readonly<Ref<boolean>>
|
||||||
|
guestPlanModal: Ref<GuestPlanModalHandle | null>
|
||||||
|
checkoutModal: Ref<CheckoutModalHandle | null>
|
||||||
|
onCheckoutPending: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function readPendingPurchaseIntent(): PendingPurchaseIntent | null {
|
||||||
|
if (typeof window === 'undefined') return null
|
||||||
|
const rawIntent = window.sessionStorage.getItem(PURCHASE_INTENT_STORAGE_KEY)
|
||||||
|
if (!rawIntent) return null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsedIntent = JSON.parse(rawIntent) as Partial<PendingPurchaseIntent>
|
||||||
|
if (
|
||||||
|
(parsedIntent.interval === 'monthly' ||
|
||||||
|
parsedIntent.interval === 'quarterly' ||
|
||||||
|
parsedIntent.interval === 'yearly') &&
|
||||||
|
(parsedIntent.planId === null || typeof parsedIntent.planId === 'string') &&
|
||||||
|
typeof parsedIntent.source === 'string' &&
|
||||||
|
typeof parsedIntent.ts === 'number'
|
||||||
|
) {
|
||||||
|
return parsedIntent as PendingPurchaseIntent
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function writePendingPurchaseIntent(intent: PendingPurchaseIntent) {
|
||||||
|
if (typeof window === 'undefined') return
|
||||||
|
window.sessionStorage.setItem(PURCHASE_INTENT_STORAGE_KEY, JSON.stringify(intent))
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearPendingPurchaseIntent() {
|
||||||
|
if (typeof window === 'undefined') return
|
||||||
|
window.sessionStorage.removeItem(PURCHASE_INTENT_STORAGE_KEY)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createHostingPurchaseIntentContext(
|
||||||
|
options: CreateHostingPurchaseIntentContextOptions,
|
||||||
|
): HostingPurchaseIntentContext {
|
||||||
|
const lastSelectedInterval = ref<ServerBillingInterval>('quarterly')
|
||||||
|
const lastSelectedPlanId = ref<string | null | undefined>(undefined)
|
||||||
|
const pendingResumeIntent = ref<PendingPurchaseIntent | null>(null)
|
||||||
|
|
||||||
|
function openCheckoutFromIntent(intent: PendingPurchaseIntent): boolean {
|
||||||
|
if (!options.checkoutModal.value || !options.canOpenCheckout.value) return false
|
||||||
|
|
||||||
|
lastSelectedInterval.value = intent.interval
|
||||||
|
lastSelectedPlanId.value = intent.planId
|
||||||
|
|
||||||
|
const defaultPlan =
|
||||||
|
options.availableProducts.value.find(
|
||||||
|
(product) => product?.metadata?.type === 'pyro' && product.metadata.ram === 6144,
|
||||||
|
) ??
|
||||||
|
options.availableProducts.value.find((product) => product?.metadata?.type === 'pyro') ??
|
||||||
|
options.availableProducts.value[0]
|
||||||
|
|
||||||
|
const selectedPlan =
|
||||||
|
intent.planId === null
|
||||||
|
? null
|
||||||
|
: (options.availableProducts.value.find((product) => product.id === intent.planId) ??
|
||||||
|
defaultPlan)
|
||||||
|
|
||||||
|
options.checkoutModal.value.show(intent.interval, selectedPlan)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function openPurchaseModal() {
|
||||||
|
options.guestPlanModal.value?.show(lastSelectedInterval.value, lastSelectedPlanId.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleGuestPlanContinue(payload: GuestPlanContinuePayload) {
|
||||||
|
const intent: PendingPurchaseIntent = {
|
||||||
|
interval: payload.interval,
|
||||||
|
planId: payload.planId,
|
||||||
|
source: options.intentSource,
|
||||||
|
ts: Date.now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
lastSelectedInterval.value = payload.interval
|
||||||
|
lastSelectedPlanId.value = payload.planId
|
||||||
|
|
||||||
|
if (!options.loggedIn.value) {
|
||||||
|
writePendingPurchaseIntent(intent)
|
||||||
|
void options.authRequestSignIn(options.signInRedirectPath)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingResumeIntent.value = intent
|
||||||
|
if (openCheckoutFromIntent(intent)) {
|
||||||
|
pendingResumeIntent.value = null
|
||||||
|
} else {
|
||||||
|
options.onCheckoutPending()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
options.loggedIn,
|
||||||
|
(isLoggedIn) => {
|
||||||
|
if (!isLoggedIn) return
|
||||||
|
|
||||||
|
const pendingIntent = readPendingPurchaseIntent()
|
||||||
|
if (!pendingIntent || pendingIntent.source !== options.intentSource) return
|
||||||
|
|
||||||
|
clearPendingPurchaseIntent()
|
||||||
|
pendingResumeIntent.value = pendingIntent
|
||||||
|
lastSelectedInterval.value = pendingIntent.interval
|
||||||
|
lastSelectedPlanId.value = pendingIntent.planId
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() =>
|
||||||
|
[
|
||||||
|
options.loggedIn.value,
|
||||||
|
options.canOpenCheckout.value,
|
||||||
|
pendingResumeIntent.value,
|
||||||
|
options.checkoutModal.value,
|
||||||
|
] as const,
|
||||||
|
([isLoggedIn, canOpen, pendingIntent]) => {
|
||||||
|
if (!isLoggedIn || !canOpen || !pendingIntent) return
|
||||||
|
if (openCheckoutFromIntent(pendingIntent)) {
|
||||||
|
pendingResumeIntent.value = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
openPurchaseModal,
|
||||||
|
handleGuestPlanContinue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const [injectHostingPurchaseIntent, provideHostingPurchaseIntent] =
|
||||||
|
createContext<HostingPurchaseIntentContext>('HostingManagePage', 'hostingPurchaseIntent')
|
||||||
@@ -4,6 +4,7 @@ export * from './auth'
|
|||||||
export * from './content-manager'
|
export * from './content-manager'
|
||||||
export { createContext } from './create-context'
|
export { createContext } from './create-context'
|
||||||
export * from './file-picker'
|
export * from './file-picker'
|
||||||
|
export * from './hosting-purchase-intent'
|
||||||
export * from './i18n'
|
export * from './i18n'
|
||||||
export * from './instance-import'
|
export * from './instance-import'
|
||||||
export * from './modal-behavior'
|
export * from './modal-behavior'
|
||||||
|
|||||||
Reference in New Issue
Block a user