fix pricing + stripe payments

This commit is contained in:
Emily
2024-09-05 16:56:21 +02:00
parent 8e56069b1a
commit 30b3ed80e2
10 changed files with 326 additions and 19 deletions

View File

@@ -13,11 +13,11 @@ export type PricingCardProp = {
planId: number planId: number
} }
const props = defineProps<{ datas: PricingCardProp[] }>(); const props = defineProps<{ datas: PricingCardProp[], defaultIndex?: number }>();
const activeProject = useActiveProject(); const activeProject = useActiveProject();
const currentIndex = ref<number>(0); const currentIndex = ref<number>(props.defaultIndex || 0);
const data = computed(() => { const data = computed(() => {
return props.datas[currentIndex.value]; return props.datas[currentIndex.value];
@@ -37,13 +37,19 @@ async function onUpgradeClick() {
<template> <template>
<div class="relative bg-[#151515] outline outline-[1px] outline-[#262626] py-8 px-10 rounded-lg w-full max-w-[30rem]"> <div
class="relative bg-[#151515] outline outline-[1px] outline-[#262626] py-8 px-10 rounded-lg w-full max-w-[30rem]">
<div class="flex flex-col gap-3 text-center"> <div class="flex flex-col gap-3 text-center pt-3">
<div class="poppins text-xl font-light"> {{ data.title }} </div> <div v-if="data.active"
<div v-if="data.active" class="absolute right-6 top-3 poppins text-[.75rem] bg-[#222A42] outline outline-[1px] outline-[#5680F8] px-3 py-[.1rem] rounded-xl"> class="absolute right-6 top-3 poppins text-[.75rem] bg-[#222A42] outline outline-[1px] outline-[#5680F8] px-3 py-[.1rem] rounded-sm">
Active Active
</div> </div>
<div v-if="!data.active && data.title === 'Growth'"
class="absolute right-6 top-3 poppins text-[.75rem] bg-[#fbbe244f] outline outline-[1px] outline-[#fbbf24] px-3 py-[.1rem] rounded-sm">
Most popular
</div>
<div class="poppins text-xl font-light"> {{ data.title }} </div>
<div class="poppins text-4xl font-medium"> {{ data.price }} </div> <div class="poppins text-4xl font-medium"> {{ data.price }} </div>
</div> </div>

View File

@@ -40,7 +40,6 @@ const customPricing: PricingCardProp[] = [
'DB instance: DEDICATED', 'DB instance: DEDICATED',
'Dedicated operator', 'Dedicated operator',
'White label', 'White label',
'Custom Charts',
'Custom Data Aggregation' 'Custom Data Aggregation'
], ],
cta: 'Let\'s Talk!', cta: 'Let\'s Talk!',
@@ -179,10 +178,22 @@ const emits = defineEmits<{
(evt: 'onCloseClick'): void (evt: 'onCloseClick'): void
}>(); }>();
const activeProject = useActiveProject()
async function onLifetimeUpgradeClick() {
const res = await $fetch<string>(`/api/pay/${activeProject.value?._id.toString()}/create-onetime`, {
...signHeaders({ 'content-type': 'application/json' }),
method: 'POST',
body: JSON.stringify({ planId: 2001 })
})
if (!res) alert('Something went wrong');
window.open(res);
}
</script> </script>
<template> <template>
<div class="p-8 overflow-y-auto xl:overflow-y-hidden"> <div class="p-8 overflow-y-auto">
<div @click="$emit('onCloseClick')" <div @click="$emit('onCloseClick')"
class="cursor-pointer fixed top-4 right-4 rounded-full bg-menu drop-shadow-[0_0_2px_#CCCCCCCC] w-9 h-9 flex items-center justify-center"> class="cursor-pointer fixed top-4 right-4 rounded-full bg-menu drop-shadow-[0_0_2px_#CCCCCCCC] w-9 h-9 flex items-center justify-center">
@@ -191,10 +202,56 @@ const emits = defineEmits<{
<div class="flex gap-8 mt-10 h-max xl:flex-row flex-col"> <div class="flex gap-8 mt-10 h-max xl:flex-row flex-col">
<PricingCardGeneric class="flex-1" :datas="freePricing"></PricingCardGeneric> <PricingCardGeneric class="flex-1" :datas="freePricing"></PricingCardGeneric>
<PricingCardGeneric class="flex-1" :datas="slidePricings"></PricingCardGeneric> <PricingCardGeneric class="flex-1" :datas="slidePricings" :default-index="2"></PricingCardGeneric>
<PricingCardGeneric class="flex-1" :datas="customPricing"></PricingCardGeneric> <PricingCardGeneric class="flex-1" :datas="customPricing"></PricingCardGeneric>
</div> </div>
<LyxUiCard class="w-full mt-6">
<div class="flex">
<div class="flex flex-col gap-3">
<div>
<span class="text-lyx-primary font-semibold text-[1.4rem]">
LIFETIME DEAL
</span>
<span class="text-lyx-text-dark text-[.8rem]"> (Growh plan) </span>
</div>
<div class="text-[2rem]"> 2.399,00 </div>
<div> Up to 500.000 visits/events per month </div>
<LyxUiButton type="primary" @click="onLifetimeUpgradeClick()"> Purchase </LyxUiButton>
</div>
<div class="flex justify-evenly grow">
<div class="flex flex-col justify-evenly">
<div class="flex items-center gap-2">
<img class="h-6" :src="'/check.png'" alt="Check">
<div> Slack support </div>
</div>
<div class="flex items-center gap-2">
<img class="h-6" :src="'/check.png'" alt="Check">
<div> Unlimited domanis </div>
</div>
<div class="flex items-center gap-2">
<img class="h-6" :src="'/check.png'" alt="Check">
<div> Unlimited reports </div>
</div>
</div>
<div class="flex flex-col justify-evenly">
<div class="flex items-center gap-2">
<img class="h-6" :src="'/check.png'" alt="Check">
<div> AI Tokens: 3.000 / month </div>
</div>
<div class="flex items-center gap-2">
<img class="h-6" :src="'/check.png'" alt="Check">
<div> Server type: SHARED </div>
</div>
<div class="flex items-center gap-2">
<img class="h-6" :src="'/check.png'" alt="Check">
<div> Data retention: 1 Year </div>
</div>
</div>
</div>
</div>
</LyxUiCard>
<div class="flex justify-between items-center mt-10 flex-col xl:flex-row"> <div class="flex justify-between items-center mt-10 flex-col xl:flex-row">
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<div class="poppins text-[2rem] font-semibold"> <div class="poppins text-[2rem] font-semibold">
@@ -212,5 +269,8 @@ const emits = defineEmits<{
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -0,0 +1,40 @@
import { getPlanFromId } from "@data/PREMIUM";
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
import StripeService from '~/server/services/StripeService';
export default defineEventHandler(async event => {
const project_id = getRequestProjectId(event);
if (!project_id) return;
const user = getRequestUser(event);
const project = await getUserProjectFromId(project_id, user);
if (!project) return;
const body = await readBody(event);
const { planId } = body;
const PLAN = getPlanFromId(planId);
if (!PLAN) {
console.error('PLAN', planId, 'NOT EXIST');
return setResponseStatus(event, 400, 'Plan not exist');
}
const intent = await StripeService.createOnetimePayment(
StripeService.testMode ? PLAN.PRICE_TEST : PLAN.PRICE,
'https://dashboard.litlyx.com/payment_ok',
project_id,
project.customer_id
)
if (!intent) {
console.error('Cannot create Intent', { plan: PLAN });
return setResponseStatus(event, 400, 'Cannot create intent');
}
return intent.url;
});

View File

@@ -63,6 +63,36 @@ async function onPaymentFailed(event: Event.InvoicePaymentFailedEvent) {
} }
async function onPaymentOnetimeSuccess(event: Event.PaymentIntentSucceededEvent) {
const customer_id = event.data.object.customer as string;
const project = await ProjectModel.findOne({ customer_id });
if (!project) return { error: 'CUSTOMER NOT EXIST' }
if (event.data.object.status === 'succeeded') {
const PLAN = getPlanFromPrice(event.data.object.metadata.price, StripeService.testMode || false);
if (!PLAN) return { error: 'Plan not found' }
const dummyPlan = PLAN.ID + 3000;
const subscription = await StripeService.createOneTimeSubscriptionDummy(customer_id, dummyPlan);
if (!subscription) return { error: 'Error creating subscription' }
const allSubscriptions = await StripeService.getAllSubscriptions(customer_id);
if (!allSubscriptions) return;
for (const subscription of allSubscriptions.data) {
if (subscription.id === subscription.id) continue;
await StripeService.deleteSubscription(subscription.id);
}
await addSubscriptionToProject(project._id.toString(), PLAN, subscription.id, subscription.current_period_start, subscription.current_period_end)
return { ok: true };
}
return { received: true, warn: 'object status not succeeded' }
}
async function onPaymentSuccess(event: Event.InvoicePaidEvent) { async function onPaymentSuccess(event: Event.InvoicePaidEvent) {
const customer_id = event.data.object.customer as string; const customer_id = event.data.object.customer as string;
@@ -76,7 +106,7 @@ async function onPaymentSuccess(event: Event.InvoicePaidEvent) {
const allSubscriptions = await StripeService.getAllSubscriptions(customer_id); const allSubscriptions = await StripeService.getAllSubscriptions(customer_id);
if (!allSubscriptions) return; if (!allSubscriptions) return;
const currentSubscription = allSubscriptions.data.find(e => e.id === subscription_id); const currentSubscription = allSubscriptions.data.find(e => e.id === subscription_id);
if (!currentSubscription) return { error: 'SUBSCRIPTION NOT EXIST' } if (!currentSubscription) return { error: 'SUBSCRIPTION NOT EXIST' }
@@ -201,7 +231,11 @@ export default defineEventHandler(async event => {
const eventData = StripeService.parseWebhook(body, signature); const eventData = StripeService.parseWebhook(body, signature);
if (!eventData) return; if (!eventData) return;
// console.log('WEBHOOK FIRED', eventData.type);
if (eventData.type === 'invoice.paid') return await onPaymentSuccess(eventData); if (eventData.type === 'invoice.paid') return await onPaymentSuccess(eventData);
if (eventData.type === 'payment_intent.succeeded') return await onPaymentOnetimeSuccess(eventData);
if (eventData.type === 'invoice.payment_failed') return await onPaymentFailed(eventData); if (eventData.type === 'invoice.payment_failed') return await onPaymentFailed(eventData);
if (eventData.type === 'customer.subscription.deleted') return await onSubscriptionDeleted(eventData); if (eventData.type === 'customer.subscription.deleted') return await onSubscriptionDeleted(eventData);
if (eventData.type === 'customer.subscription.created') return await onSubscriptionCreated(eventData); if (eventData.type === 'customer.subscription.created') return await onSubscriptionCreated(eventData);

View File

@@ -16,6 +16,25 @@ export default defineEventHandler(async event => {
const project = await ProjectModel.findById(project_id); const project = await ProjectModel.findById(project_id);
if (!project) return setResponseStatus(event, 400, 'Project not found'); if (!project) return setResponseStatus(event, 400, 'Project not found');
if (project.subscription_id === 'onetime') {
const projectLimits = await ProjectLimitModel.findOne({ project_id });
if (!projectLimits) return setResponseStatus(event, 400, 'Project limits not found');
const result = {
premium: project.premium,
premium_type: project.premium_type,
billing_start_at: projectLimits.billing_start_at,
billing_expire_at: projectLimits.billing_expire_at,
limit: projectLimits.limit,
count: projectLimits.events + projectLimits.visits,
subscription_status: StripeService.isDisabled() ? 'Disabled mode' : ('One time payment')
}
return result;
}
const subscription = await StripeService.getSubscription(project.subscription_id); const subscription = await StripeService.getSubscription(project.subscription_id);
const projectLimits = await ProjectLimitModel.findOne({ project_id }); const projectLimits = await ProjectLimitModel.findOne({ project_id });

View File

@@ -1,4 +1,4 @@
import { getPlanFromTag } from '@data/PREMIUM'; import { getPlanFromId, getPlanFromTag } from '@data/PREMIUM';
import Stripe from 'stripe'; import Stripe from 'stripe';
class StripeService { class StripeService {
@@ -29,6 +29,33 @@ class StripeService {
return this.stripe.webhooks.constructEvent(body, sig, this.webhookSecret); return this.stripe.webhooks.constructEvent(body, sig, this.webhookSecret);
} }
async createOnetimePayment(price: string, success_url: string, pid: string, customer?: string) {
if (this.disabledMode) return;
if (!this.stripe) throw Error('Stripe not initialized');
const checkout = await this.stripe.checkout.sessions.create({
allow_promotion_codes: true,
payment_method_types: ['card'],
invoice_creation: {
enabled: true,
},
line_items: [
{ price, quantity: 1 }
],
payment_intent_data: {
metadata: {
pid, price
}
},
customer,
success_url,
mode: 'payment'
});
return checkout;
}
async cretePayment(price: string, success_url: string, pid: string, customer?: string) { async cretePayment(price: string, success_url: string, pid: string, customer?: string) {
if (this.disabledMode) return; if (this.disabledMode) return;
if (!this.stripe) throw Error('Stripe not initialized'); if (!this.stripe) throw Error('Stripe not initialized');
@@ -50,6 +77,13 @@ class StripeService {
return checkout; return checkout;
} }
async getPriceData(priceId: string) {
if (this.disabledMode) return;
if (!this.stripe) throw Error('Stripe not initialized');
const priceData = await this.stripe.prices.retrieve(priceId);
return priceData;
}
async deleteSubscription(subscriptionId: string) { async deleteSubscription(subscriptionId: string) {
if (this.disabledMode) return; if (this.disabledMode) return;
if (!this.stripe) throw Error('Stripe not initialized'); if (!this.stripe) throw Error('Stripe not initialized');
@@ -78,7 +112,6 @@ class StripeService {
return invoices; return invoices;
} }
async getCustomer(customer_id: string) { async getCustomer(customer_id: string) {
if (this.disabledMode) return; if (this.disabledMode) return;
if (!this.stripe) throw Error('Stripe not initialized'); if (!this.stripe) throw Error('Stripe not initialized');
@@ -100,8 +133,27 @@ class StripeService {
return deleted; return deleted;
} }
async createOneTimeCoupon() {
if (this.disabledMode) return;
if (!this.stripe) throw Error('Stripe not initialized');
}
async createOneTimeSubscriptionDummy(customer_id: string, planId: number) {
if (this.disabledMode) return;
if (!this.stripe) throw Error('Stripe not initialized');
const PLAN = getPlanFromId(planId);
if (!PLAN) throw Error('Plan not found');
const subscription = await this.stripe.subscriptions.create({
customer: customer_id,
items: [
{ price: this.testMode ? PLAN.PRICE_TEST : PLAN.PRICE, quantity: 1 }
],
});
return subscription;
}
async createFreeSubscription(customer_id: string) { async createFreeSubscription(customer_id: string) {
if (this.disabledMode) return; if (this.disabledMode) return;

View File

@@ -84,11 +84,9 @@ onMounted(() => {
</div> </div>
<div class="px-10 pt-6 lg:pt-0"> <div class="px-10 pt-6 lg:pt-0">
<MainButton link="https://dashboard.litlyx.com" class="!whitespace-nowrap">
<button class="poppins w-fit cursor-pointer px-4 py-1 rounded-md outline outline-[1px] text-text bg-lyx-primary-dark outline-lyx-primary hover:bg-lyx-primary-hover button !whitespace-nowrap" onclick="window.location='https://dashboard.litlyx.com';">
Get started Get started
</button> </MainButton>
</div> </div>
</div> </div>

35
landing/nuxt.config.ts Normal file
View File

@@ -0,0 +1,35 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
colorMode: { preference: 'dark', },
devtools: { enabled: false },
app: {
head: {
script: [
{
src: 'https://cdn.jsdelivr.net/gh/litlyx/litlyx-js/browser/litlyx.js',
'data-project': '6643cd08a1854e3b81722ab5',
defer: true
}
]
}
},
pages: true,
ssr: true,
routeRules: {
'/': {
prerender: true
},
'/**': {
prerender: true
},
},
css: ['~/assets/scss/main.scss'],
modules: ['@nuxt/ui'],
devServer: {
host: '0.0.0.0',
},
components: true,
extends: ['../lyx-ui']
})

View File

@@ -40,7 +40,6 @@ const customPricing: PricingCardProp[] = [
'DB instance: DEDICATED', 'DB instance: DEDICATED',
'Dedicated operator', 'Dedicated operator',
'White label', 'White label',
'Custom Charts',
'Custom Data Aggregation' 'Custom Data Aggregation'
], ],
cta: 'Let\'s Talk!', cta: 'Let\'s Talk!',
@@ -182,6 +181,54 @@ const slidePricings: PricingCardProp[] = [
</PricingCardGeneric> </PricingCardGeneric>
</div> </div>
<div class="flex justify-center">
<LyxUiCard class="w-full mt-6 max-w-[96rem]">
<div class="flex flex-col lg:flex-row">
<div class="flex flex-col gap-3">
<div>
<span class="text-lyx-primary font-semibold text-[1.4rem]">
LIFETIME DEAL
</span>
<span class="text-lyx-text-dark text-[.8rem]"> (Growh plan) </span>
</div>
<div class="text-[2rem]"> 2.399,00 </div>
<div> Up to 500.000 visits/events per month </div>
<LyxUiButton type="primary" link="https://dashboard.litlyx.com"> Start for free now </LyxUiButton>
</div>
<div class="flex justify-evenly grow flex-col gap-2 lg:gap-0 lg:flex-row mt-4 lg:mt-0">
<div class="flex flex-col justify-evenly gap-2 lg:gap-0">
<div class="flex items-center gap-2">
<img class="h-6" :src="'/check.png'" alt="Check">
<div> Slack support </div>
</div>
<div class="flex items-center gap-2">
<img class="h-6" :src="'/check.png'" alt="Check">
<div> Unlimited domanis </div>
</div>
<div class="flex items-center gap-2">
<img class="h-6" :src="'/check.png'" alt="Check">
<div> Unlimited reports </div>
</div>
</div>
<div class="flex flex-col justify-evenly gap-2 lg:gap-0">
<div class="flex items-center gap-2">
<img class="h-6" :src="'/check.png'" alt="Check">
<div> AI Tokens: 3.000 / month </div>
</div>
<div class="flex items-center gap-2">
<img class="h-6" :src="'/check.png'" alt="Check">
<div> Server type: SHARED </div>
</div>
<div class="flex items-center gap-2">
<img class="h-6" :src="'/check.png'" alt="Check">
<div> Data retention: 1 Year </div>
</div>
</div>
</div>
</div>
</LyxUiCard>
</div>
<!-- <div class="flex gap-8 h-max flex-col lg:flex-row"> <!-- <div class="flex gap-8 h-max flex-col lg:flex-row">
<PricingCard class="flex-1" :data="starterTierCardData"></PricingCard> <PricingCard class="flex-1" :data="starterTierCardData"></PricingCard>
<PricingCard class="flex-1" :data="accelerationTierCardData"></PricingCard> <PricingCard class="flex-1" :data="accelerationTierCardData"></PricingCard>
@@ -233,7 +280,7 @@ const slidePricings: PricingCardProp[] = [
<UAccordion :ui="{ <UAccordion :ui="{
wrapper: 'w-full', wrapper: 'w-full',
item: { item: {
padding: 'pl-8' padding: 'pl-8',
} }
}" color="white" variant="ghost" size="xl" :items="[ }" color="white" variant="ghost" size="xl" :items="[
{ {

View File

@@ -12,7 +12,9 @@ export const PREMIUM_TAGS = [
'GROWTH', 'GROWTH',
'EXPANSION', 'EXPANSION',
'SCALING', 'SCALING',
'UNICORN' 'UNICORN',
'LIFETIME_GROWTH_ONETIME',
'GROWTH_DUMMY'
] as const; ] as const;
@@ -95,6 +97,20 @@ export const PREMIUM_PLAN: Record<PREMIUM_TAG, PREMIUM_DATA> = {
PRICE: 'price_1Pdt2LB2lPUiVs9VGBFAIG9G', PRICE: 'price_1Pdt2LB2lPUiVs9VGBFAIG9G',
PRICE_TEST: '' PRICE_TEST: ''
}, },
LIFETIME_GROWTH_ONETIME: {
ID: 2001,
COUNT_LIMIT: 500_000,
AI_MESSAGE_LIMIT: 3_000,
PRICE: 'price_1PvewGB2lPUiVs9VLheJC8s1',
PRICE_TEST: 'price_1Pvf7LB2lPUiVs9VMFNyzpim'
},
GROWTH_DUMMY: {
ID: 5001,
COUNT_LIMIT: 500_000,
AI_MESSAGE_LIMIT: 3_000,
PRICE: 'price_1PvgoRB2lPUiVs9VC51YBT7J',
PRICE_TEST: 'price_1PvgRTB2lPUiVs9V3kFSNC3G'
}
} }
CustomPremiumPriceModel.find({}).then(custom_prices => { CustomPremiumPriceModel.find({}).then(custom_prices => {