feat: migrate from lemonsqueezy to polar (#535)
* feat: move from lemonsqueezy to polar * update lock * add migration files * test with demo product id
This commit is contained in:
Vendored
+3
@@ -20,5 +20,8 @@
|
|||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.organizeImports": "explicit"
|
"source.organizeImports": "explicit"
|
||||||
|
},
|
||||||
|
"[svelte]": {
|
||||||
|
"editor.defaultFormatter": "svelte.svelte-vscode"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,7 @@
|
|||||||
"openai-edge": "^1.2.2",
|
"openai-edge": "^1.2.2",
|
||||||
"papaparse": "^5.4.1",
|
"papaparse": "^5.4.1",
|
||||||
"pluralize": "^8.0.0",
|
"pluralize": "^8.0.0",
|
||||||
|
"polar-sveltekit": "^0.0.4",
|
||||||
"posthog-js": "^1.180.1",
|
"posthog-js": "^1.180.1",
|
||||||
"shared": "workspace:*",
|
"shared": "workspace:*",
|
||||||
"sirv": "^2.0.3",
|
"sirv": "^2.0.3",
|
||||||
|
|||||||
@@ -1,32 +1,30 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount, onDestroy } from 'svelte';
|
|
||||||
import type { OrganizationPlan } from '$lib/utils/types';
|
|
||||||
import { supabase } from '$lib/utils/functions/supabase';
|
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { currentOrg, currentOrgPath } from '$lib/utils/store/org';
|
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import Checkmark from 'carbon-icons-svelte/lib/Checkmark.svelte';
|
|
||||||
import PLANS from 'shared/src/plans/data.json';
|
|
||||||
import { profile } from '$lib/utils/store/user';
|
|
||||||
import { subscribeToProduct } from '$lib/utils/services/lemonsqueezy/subscribe';
|
|
||||||
import { snackbar } from '$lib/components/Snackbar/store';
|
|
||||||
import { Loading } from 'carbon-components-svelte';
|
|
||||||
import Modal from '$lib/components/Modal/index.svelte';
|
|
||||||
import StepDoneIcon from '$lib/components/Icons/StepDoneIcon.svelte';
|
|
||||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
|
||||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
|
||||||
import type { RealtimeChannel, RealtimePostgresChangesPayload } from '@supabase/supabase-js';
|
|
||||||
import Confetti from '$lib/components/Confetti/index.svelte';
|
import Confetti from '$lib/components/Confetti/index.svelte';
|
||||||
import { toggleConfetti } from '$lib/components/Confetti/store';
|
import { toggleConfetti } from '$lib/components/Confetti/store';
|
||||||
|
|
||||||
|
import StepDoneIcon from '$lib/components/Icons/StepDoneIcon.svelte';
|
||||||
|
import Modal from '$lib/components/Modal/index.svelte';
|
||||||
|
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||||
|
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||||
|
import { snackbar } from '$lib/components/Snackbar/store';
|
||||||
import { t } from '$lib/utils/functions/translations';
|
import { t } from '$lib/utils/functions/translations';
|
||||||
|
import { currentOrg, currentOrgPath, isFreePlan } from '$lib/utils/store/org';
|
||||||
|
import { profile } from '$lib/utils/store/user';
|
||||||
|
import { Loading } from 'carbon-components-svelte';
|
||||||
|
import Checkmark from 'carbon-icons-svelte/lib/Checkmark.svelte';
|
||||||
|
import PLANS from 'shared/src/plans/data.json';
|
||||||
|
|
||||||
const disabledClass = 'bg-gray-300 text-gray-400 hover:cursor-not-allowed';
|
const disabledClass = 'bg-gray-300 text-gray-400 hover:cursor-not-allowed';
|
||||||
|
|
||||||
|
const planNames = Object.keys(PLANS);
|
||||||
|
|
||||||
let isLoadingPlan: string | null = null;
|
let isLoadingPlan: string | null = null;
|
||||||
let orgPlanChannel: RealtimeChannel;
|
|
||||||
let open = false;
|
let open = false;
|
||||||
let upgraded = false;
|
let upgraded = false;
|
||||||
let isYearlyPlan = false;
|
let isYearlyPlan = false;
|
||||||
|
let isConfirming = false;
|
||||||
|
|
||||||
async function handleClick(plan, planName: string) {
|
async function handleClick(plan, planName: string) {
|
||||||
if (plan.CTA.IS_DISABLED || !$profile.id) {
|
if (plan.CTA.IS_DISABLED || !$profile.id) {
|
||||||
@@ -41,20 +39,24 @@
|
|||||||
isLoadingPlan = planName;
|
isLoadingPlan = planName;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { checkoutURL } = await subscribeToProduct({
|
const checkoutURL = new URL('/api/polar/subscribe', $page.url);
|
||||||
productId: isYearlyPlan ? plan.CTA.PRODUCT_ID_YEARLY : plan.CTA.PRODUCT_ID,
|
|
||||||
email: $profile.email,
|
|
||||||
name: $profile.fullname,
|
|
||||||
triggeredBy: $currentOrg.memberId,
|
|
||||||
orgId: $currentOrg.id
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!checkoutURL) {
|
checkoutURL.searchParams.set(
|
||||||
snackbar.error('snackbar.upgrade.generate_fail');
|
'productId',
|
||||||
return;
|
isYearlyPlan ? plan.CTA.PRODUCT_ID_YEARLY : plan.CTA.PRODUCT_ID
|
||||||
}
|
);
|
||||||
|
checkoutURL.searchParams.set('customerEmail', $profile.email);
|
||||||
|
checkoutURL.searchParams.set('customerName', $profile.fullname);
|
||||||
|
checkoutURL.searchParams.set(
|
||||||
|
'metadata',
|
||||||
|
JSON.stringify({
|
||||||
|
triggeredBy: $currentOrg.memberId,
|
||||||
|
orgId: $currentOrg.id,
|
||||||
|
orgSlug: $currentOrg.siteName
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
window.open(checkoutURL, '_blank');
|
window.location.href = checkoutURL.toString();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error subscribing', error);
|
console.error('Error subscribing', error);
|
||||||
|
|
||||||
@@ -64,27 +66,18 @@
|
|||||||
isLoadingPlan = null;
|
isLoadingPlan = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleInsert(payload: RealtimePostgresChangesPayload<OrganizationPlan>) {
|
function onUpgrade() {
|
||||||
const newPlan = payload.new as OrganizationPlan;
|
upgraded = true;
|
||||||
|
toggleConfetti();
|
||||||
|
|
||||||
console.log('A new plan was inserted');
|
setTimeout(() => {
|
||||||
// If plan was successfully generated
|
|
||||||
if (newPlan.org_id === $currentOrg.id && newPlan.is_active) {
|
|
||||||
upgraded = true;
|
|
||||||
toggleConfetti();
|
toggleConfetti();
|
||||||
|
}, 1000);
|
||||||
setTimeout(() => {
|
|
||||||
toggleConfetti();
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onClose() {
|
function onClose() {
|
||||||
if (upgraded) {
|
const path = upgraded ? $currentOrgPath : $page.url.pathname;
|
||||||
window.location.href = $currentOrgPath;
|
goto(path);
|
||||||
} else {
|
|
||||||
goto($page.url.pathname);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onLearnMore() {
|
function onLearnMore() {
|
||||||
@@ -94,28 +87,18 @@
|
|||||||
isYearlyPlan = !isYearlyPlan;
|
isYearlyPlan = !isYearlyPlan;
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
orgPlanChannel = supabase
|
|
||||||
.channel('any')
|
|
||||||
.on(
|
|
||||||
'postgres_changes',
|
|
||||||
{ event: 'INSERT', schema: 'public', table: 'organization_plan' },
|
|
||||||
handleInsert
|
|
||||||
)
|
|
||||||
.subscribe();
|
|
||||||
});
|
|
||||||
|
|
||||||
onDestroy(() => {
|
|
||||||
if (orgPlanChannel) {
|
|
||||||
supabase.removeChannel(orgPlanChannel);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
const query = new URLSearchParams($page.url.search);
|
const query = new URLSearchParams($page.url.search);
|
||||||
open = (query.get('upgrade') || '') === 'true';
|
open = (query.get('upgrade') || '') === 'true';
|
||||||
|
isConfirming = (query.get('confirmation') || '') === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
$: handleUpgradeSuccess(Boolean($currentOrg.id && !$isFreePlan && open));
|
||||||
|
function handleUpgradeSuccess(upgradeSuccessful: boolean) {
|
||||||
|
if (upgradeSuccessful) {
|
||||||
|
onUpgrade();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
$: planNames = Object.keys(PLANS);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if upgraded}
|
{#if upgraded}
|
||||||
@@ -126,12 +109,12 @@
|
|||||||
{onClose}
|
{onClose}
|
||||||
{open}
|
{open}
|
||||||
width="w-4/5"
|
width="w-4/5"
|
||||||
maxWidth={upgraded ? 'max-w-[600px]' : undefined}
|
maxWidth={upgraded || isConfirming ? 'max-w-[600px]' : undefined}
|
||||||
modalHeading={$t('pricing.modal.heading')}
|
modalHeading={$t('pricing.modal.heading')}
|
||||||
containerClass="pt-4"
|
containerClass="pt-4"
|
||||||
>
|
>
|
||||||
{#if upgraded}
|
{#if upgraded}
|
||||||
<div class="flex flex-col items-center justify-center mb-4 w-full relative">
|
<div class="relative mb-4 flex w-full flex-col items-center justify-center">
|
||||||
<StepDoneIcon />
|
<StepDoneIcon />
|
||||||
<h4 class="text-2xl">{$t('pricing.modal.thanks')}</h4>
|
<h4 class="text-2xl">{$t('pricing.modal.thanks')}</h4>
|
||||||
<p class="mb-4 text-center">
|
<p class="mb-4 text-center">
|
||||||
@@ -150,6 +133,10 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{:else if isConfirming}
|
||||||
|
<div class="relative mb-4 flex w-full flex-col items-center justify-center">
|
||||||
|
<Loading withOverlay={false} />
|
||||||
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="flex flex-col items-center justify-center">
|
<div class="flex flex-col items-center justify-center">
|
||||||
<div class="relative mb-2 flex items-center rounded-[30px] border-[2px] p-[2px] lg:scale-100">
|
<div class="relative mb-2 flex items-center rounded-[30px] border-[2px] p-[2px] lg:scale-100">
|
||||||
@@ -157,7 +144,7 @@
|
|||||||
style="background-color: {isYearlyPlan ? 'initial' : '#1D4EE2'}; color: {isYearlyPlan
|
style="background-color: {isYearlyPlan ? 'initial' : '#1D4EE2'}; color: {isYearlyPlan
|
||||||
? '#5e636b'
|
? '#5e636b'
|
||||||
: '#fff'}"
|
: '#fff'}"
|
||||||
class="rounded-[30px] bg-blue-700 px-3 py-1 text-xs text-white lg:px-4 lg:py-2 transition-all duration-500 ease-in-out"
|
class="rounded-[30px] bg-blue-700 px-3 py-1 text-xs text-white transition-all duration-500 ease-in-out lg:px-4 lg:py-2"
|
||||||
on:click={toggleIsYearlyPlan}
|
on:click={toggleIsYearlyPlan}
|
||||||
>
|
>
|
||||||
{$t('pricing.modal.monthly')}
|
{$t('pricing.modal.monthly')}
|
||||||
@@ -166,12 +153,12 @@
|
|||||||
style="background-color: {isYearlyPlan ? '#1D4EE2' : ''}; color: {isYearlyPlan
|
style="background-color: {isYearlyPlan ? '#1D4EE2' : ''}; color: {isYearlyPlan
|
||||||
? '#fff'
|
? '#fff'
|
||||||
: '#5e636b'}"
|
: '#5e636b'}"
|
||||||
class="relative rounded-[30px] px-3 py-1 text-xs text-white lg:px-4 lg:py-2 transition-all duration-500 ease-in-out"
|
class="relative rounded-[30px] px-3 py-1 text-xs text-white transition-all duration-500 ease-in-out lg:px-4 lg:py-2"
|
||||||
on:click={toggleIsYearlyPlan}
|
on:click={toggleIsYearlyPlan}
|
||||||
>
|
>
|
||||||
{$t('pricing.modal.annually')}
|
{$t('pricing.modal.annually')}
|
||||||
<div
|
<div
|
||||||
class="absolute right-[-40%] -top-4 scale-[90%] rounded-full bg-[#006600] px-1.5 py-1 text-[0.7rem] text-white"
|
class="absolute -top-4 right-[-40%] scale-[90%] rounded-full bg-[#006600] px-1.5 py-1 text-[0.7rem] text-white"
|
||||||
>
|
>
|
||||||
{$t('pricing.modal.save')}
|
{$t('pricing.modal.save')}
|
||||||
</div>
|
</div>
|
||||||
@@ -213,7 +200,7 @@
|
|||||||
? 'bg-white text-slate-900 hover:bg-indigo-50'
|
? 'bg-white text-slate-900 hover:bg-indigo-50'
|
||||||
: PLANS[planName].CTA.IS_DISABLED
|
: PLANS[planName].CTA.IS_DISABLED
|
||||||
? disabledClass
|
? disabledClass
|
||||||
: 'bg-primary-900 hover:bg-primary-700 text-white'} py-3 text-center font-medium hover:no-underline lg:rounded-md lg:py-3 lg:text-lg lg:font-semibold flex items-center justify-center"
|
: 'bg-primary-900 hover:bg-primary-700 text-white'} flex items-center justify-center py-3 text-center font-medium hover:no-underline lg:rounded-md lg:py-3 lg:text-lg lg:font-semibold"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
if (isLoadingPlan === planName) return;
|
if (isLoadingPlan === planName) return;
|
||||||
|
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ export async function getOrganizations(userId: string, isOrgSite?: boolean, orgS
|
|||||||
organization_plan(
|
organization_plan(
|
||||||
plan_name,
|
plan_name,
|
||||||
is_active,
|
is_active,
|
||||||
subscriptionId:lmz_data->id
|
subscriptionId:payload->id
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
`
|
`
|
||||||
@@ -255,11 +255,27 @@ export async function getCurrentOrg(siteName: string, justGet = false, isCustomD
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateOrgPlan(params: {
|
||||||
|
supabase: typeof supabase;
|
||||||
|
subscriptionId: string;
|
||||||
|
data: OrganizationPlan['payload'];
|
||||||
|
}) {
|
||||||
|
return await params.supabase
|
||||||
|
.from('organization_plan')
|
||||||
|
.update({
|
||||||
|
payload: params.data
|
||||||
|
})
|
||||||
|
.match({
|
||||||
|
subscription_id: params.subscriptionId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function createOrgPlan(params: {
|
export async function createOrgPlan(params: {
|
||||||
orgId: string;
|
orgId: OrganizationPlan['org_id'];
|
||||||
planName: string;
|
planName: OrganizationPlan['plan_name'];
|
||||||
triggeredBy: number;
|
subscriptionId: OrganizationPlan['subscription_id'];
|
||||||
data: OrganizationPlan['lmz_data'];
|
triggeredBy: OrganizationPlan['triggered_by'];
|
||||||
|
data: OrganizationPlan['payload'];
|
||||||
supabase: typeof supabase;
|
supabase: typeof supabase;
|
||||||
}) {
|
}) {
|
||||||
return await params.supabase.from('organization_plan').insert({
|
return await params.supabase.from('organization_plan').insert({
|
||||||
@@ -268,19 +284,24 @@ export async function createOrgPlan(params: {
|
|||||||
triggered_by: params.triggeredBy,
|
triggered_by: params.triggeredBy,
|
||||||
plan_name: params.planName,
|
plan_name: params.planName,
|
||||||
is_active: true,
|
is_active: true,
|
||||||
lmz_data: params.data
|
payload: params.data,
|
||||||
|
subscription_id: params.subscriptionId,
|
||||||
|
provider: 'polar'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function cancelOrgPlan(params: { orgId: string; planName: string }) {
|
export async function cancelOrgPlan(params: {
|
||||||
|
subscriptionId: string;
|
||||||
|
data: OrganizationPlan['payload'];
|
||||||
|
}) {
|
||||||
return await supabase
|
return await supabase
|
||||||
.from('organization_plan')
|
.from('organization_plan')
|
||||||
.update({
|
.update({
|
||||||
is_active: false,
|
is_active: false,
|
||||||
deactivated_at: new Date().toDateString()
|
deactivated_at: new Date().toDateString(),
|
||||||
|
payload: params.data
|
||||||
})
|
})
|
||||||
.match({
|
.match({
|
||||||
plan_name: params.planName,
|
subscription_id: params.subscriptionId
|
||||||
org_id: params.orgId
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,10 +136,13 @@ export interface OrganizationPlan {
|
|||||||
activated_at: string;
|
activated_at: string;
|
||||||
deactivated_at: string;
|
deactivated_at: string;
|
||||||
org_id: string;
|
org_id: string;
|
||||||
|
subscription_id: string;
|
||||||
|
triggered_by: number;
|
||||||
plan_name: string;
|
plan_name: string;
|
||||||
is_active: boolean;
|
is_active: boolean;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
lmz_data: unknown;
|
payload: unknown;
|
||||||
|
provider: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Profile {
|
export interface Profile {
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
interface BillingAddress {
|
||||||
|
line1: string | null;
|
||||||
|
line2: string | null;
|
||||||
|
postalCode: string | null;
|
||||||
|
city: string | null;
|
||||||
|
state: string | null;
|
||||||
|
country: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Customer {
|
||||||
|
createdAt: string;
|
||||||
|
modifiedAt: string | null;
|
||||||
|
id: string;
|
||||||
|
metadata: Record<string, any>;
|
||||||
|
email: string;
|
||||||
|
emailVerified: boolean;
|
||||||
|
name: string;
|
||||||
|
billingAddress: BillingAddress;
|
||||||
|
taxId: string | null;
|
||||||
|
organizationId: string;
|
||||||
|
avatarUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
publicName: string;
|
||||||
|
avatarUrl: string | null;
|
||||||
|
githubUsername: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Price {
|
||||||
|
createdAt: string;
|
||||||
|
modifiedAt: string | null;
|
||||||
|
id: string;
|
||||||
|
amountType: string;
|
||||||
|
isArchived: boolean;
|
||||||
|
productId: string;
|
||||||
|
priceCurrency: string;
|
||||||
|
priceAmount: number;
|
||||||
|
type: string;
|
||||||
|
recurringInterval: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Media {
|
||||||
|
id: string;
|
||||||
|
organizationId: string;
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
mimeType: string;
|
||||||
|
size: number;
|
||||||
|
storageVersion: string;
|
||||||
|
checksumEtag: string;
|
||||||
|
checksumSha256Base64: string;
|
||||||
|
checksumSha256Hex: string;
|
||||||
|
lastModifiedAt: string;
|
||||||
|
version: string | null;
|
||||||
|
service: string;
|
||||||
|
isUploaded: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
sizeReadable: string;
|
||||||
|
publicUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Product {
|
||||||
|
createdAt: string;
|
||||||
|
modifiedAt: string;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
isRecurring: boolean;
|
||||||
|
isArchived: boolean;
|
||||||
|
organizationId: string;
|
||||||
|
metadata: Record<string, any>;
|
||||||
|
prices: Price[];
|
||||||
|
benefits: any[];
|
||||||
|
medias: Media[];
|
||||||
|
attachedCustomFields: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SubscriptionData {
|
||||||
|
createdAt: string;
|
||||||
|
modifiedAt: string | null;
|
||||||
|
id: string;
|
||||||
|
amount: number;
|
||||||
|
currency: string;
|
||||||
|
recurringInterval: string;
|
||||||
|
status: string;
|
||||||
|
currentPeriodStart: string;
|
||||||
|
currentPeriodEnd: string;
|
||||||
|
cancelAtPeriodEnd: boolean;
|
||||||
|
canceledAt: string | null;
|
||||||
|
startedAt: string;
|
||||||
|
endsAt: string | null;
|
||||||
|
endedAt: string | null;
|
||||||
|
customerId: string;
|
||||||
|
productId: string;
|
||||||
|
priceId: string;
|
||||||
|
discountId: string | null;
|
||||||
|
checkoutId: string;
|
||||||
|
customerCancellationReason: string | null;
|
||||||
|
customerCancellationComment: string | null;
|
||||||
|
metadata: {
|
||||||
|
orgId: string;
|
||||||
|
orgSlug: string;
|
||||||
|
triggeredBy: string;
|
||||||
|
};
|
||||||
|
customFieldData: Record<string, any>;
|
||||||
|
customer: Customer;
|
||||||
|
userId: string;
|
||||||
|
user: User;
|
||||||
|
product: Product;
|
||||||
|
price: Price;
|
||||||
|
discount: any | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PolarWebhookPayload {
|
||||||
|
type: string;
|
||||||
|
data: SubscriptionData;
|
||||||
|
}
|
||||||
@@ -21,6 +21,8 @@ interface LoadOutput {
|
|||||||
serverLang: string;
|
serverLang: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const APP_SUBDOMAINS = env.PRIVATE_APP_SUBDOMAINS.split(',');
|
||||||
|
|
||||||
export const load = async ({ url, cookies, request }): Promise<LoadOutput> => {
|
export const load = async ({ url, cookies, request }): Promise<LoadOutput> => {
|
||||||
const response: LoadOutput = {
|
const response: LoadOutput = {
|
||||||
orgSiteName: '',
|
orgSiteName: '',
|
||||||
@@ -31,6 +33,8 @@ export const load = async ({ url, cookies, request }): Promise<LoadOutput> => {
|
|||||||
serverLang: request.headers?.get('accept-language') || ''
|
serverLang: request.headers?.get('accept-language') || ''
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log('IS_SELFHOSTED', IS_SELFHOSTED);
|
||||||
|
|
||||||
// Selfhosted usecase would be here
|
// Selfhosted usecase would be here
|
||||||
if (IS_SELFHOSTED === 'true') {
|
if (IS_SELFHOSTED === 'true') {
|
||||||
const subdomain = getSubdomain(url);
|
const subdomain = getSubdomain(url);
|
||||||
@@ -72,7 +76,7 @@ export const load = async ({ url, cookies, request }): Promise<LoadOutput> => {
|
|||||||
|
|
||||||
const isDev = dev || isLocalHost;
|
const isDev = dev || isLocalHost;
|
||||||
|
|
||||||
if (!url.host.includes('.classroomio.com') && !isLocalHost) {
|
if (isURLCustomDomain(url)) {
|
||||||
// Custom domain
|
// Custom domain
|
||||||
response.org = (await getCurrentOrg(url.host, true, true)) || null;
|
response.org = (await getCurrentOrg(url.host, true, true)) || null;
|
||||||
|
|
||||||
@@ -86,8 +90,15 @@ export const load = async ({ url, cookies, request }): Promise<LoadOutput> => {
|
|||||||
response.orgSiteName = response.org?.siteName || '';
|
response.orgSiteName = response.org?.siteName || '';
|
||||||
return response;
|
return response;
|
||||||
} else if (!blockedSubdomain.includes(subdomain)) {
|
} else if (!blockedSubdomain.includes(subdomain)) {
|
||||||
|
if (APP_SUBDOMAINS.includes(subdomain)) {
|
||||||
|
// This is an app domain specified in the .env file
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
const answer = !!subdomain;
|
const answer = !!subdomain;
|
||||||
|
|
||||||
|
console.log('subdomain', subdomain);
|
||||||
|
|
||||||
response.isOrgSite = debugMode || answer;
|
response.isOrgSite = debugMode || answer;
|
||||||
response.orgSiteName = debugMode ? _orgSiteName : subdomain;
|
response.orgSiteName = debugMode ? _orgSiteName : subdomain;
|
||||||
response.org = (await getCurrentOrg(response.orgSiteName, true)) || null;
|
response.org = (await getCurrentOrg(response.orgSiteName, true)) || null;
|
||||||
@@ -99,7 +110,7 @@ export const load = async ({ url, cookies, request }): Promise<LoadOutput> => {
|
|||||||
}
|
}
|
||||||
} else if (subdomain === 'play' || debugPlay === 'true') {
|
} else if (subdomain === 'play' || debugPlay === 'true') {
|
||||||
response.skipAuth = true;
|
response.skipAuth = true;
|
||||||
} else if (!env.PRIVATE_APP_SUBDOMAINS.split(',').includes(subdomain) && !isDev) {
|
} else if (!APP_SUBDOMAINS.includes(subdomain) && !isDev) {
|
||||||
// This case is for anything in our blockedSubdomains
|
// This case is for anything in our blockedSubdomains
|
||||||
throw redirect(307, 'https://app.classroomio.com');
|
throw redirect(307, 'https://app.classroomio.com');
|
||||||
}
|
}
|
||||||
@@ -107,6 +118,18 @@ export const load = async ({ url, cookies, request }): Promise<LoadOutput> => {
|
|||||||
return response;
|
return response;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function isURLCustomDomain(url: URL) {
|
||||||
|
if (url.host.includes('localhost')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const notCustomDomainHosts = [env.PRIVATE_APP_HOST || '', 'classroomio.com', 'vercel.app'].filter(
|
||||||
|
Boolean
|
||||||
|
);
|
||||||
|
|
||||||
|
return !notCustomDomainHosts.some((host) => url.host.endsWith(host));
|
||||||
|
}
|
||||||
|
|
||||||
function getBaseMetaTags(url: URL) {
|
function getBaseMetaTags(url: URL) {
|
||||||
return Object.freeze({
|
return Object.freeze({
|
||||||
title: 'ClassroomIO | The Open Source Learning Management System for Companies',
|
title: 'ClassroomIO | The Open Source Learning Management System for Companies',
|
||||||
@@ -145,8 +168,6 @@ function getBaseMetaTags(url: URL) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function getSubdomain(url: URL) {
|
function getSubdomain(url: URL) {
|
||||||
const host = url.host.replace('www.', '');
|
const host = url.host.replace('www.', '');
|
||||||
const parts = host.split('.');
|
const parts = host.split('.');
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
|
import { env } from '$env/dynamic/private';
|
||||||
|
import { getServerSupabase } from '$lib/utils/functions/supabase.server';
|
||||||
|
import { cancelOrgPlan, createOrgPlan } from '$lib/utils/services/org';
|
||||||
import { json } from '@sveltejs/kit';
|
import { json } from '@sveltejs/kit';
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import { env } from '$env/dynamic/private';
|
|
||||||
import { createOrgPlan, cancelOrgPlan } from '$lib/utils/services/org';
|
|
||||||
import { PLAN } from 'shared/src/plans/constants';
|
import { PLAN } from 'shared/src/plans/constants';
|
||||||
import { getServerSupabase } from '$lib/utils/functions/supabase.server';
|
|
||||||
|
|
||||||
export async function POST({ request }) {
|
export async function POST({ request }) {
|
||||||
try {
|
try {
|
||||||
@@ -30,10 +30,11 @@ export async function POST({ request }) {
|
|||||||
if (eventType === 'subscription_created' || eventType === 'subscription_resumed') {
|
if (eventType === 'subscription_created' || eventType === 'subscription_resumed') {
|
||||||
const orgId = body.meta.custom_data.org_id;
|
const orgId = body.meta.custom_data.org_id;
|
||||||
const triggeredBy = body.meta.custom_data.triggered_by;
|
const triggeredBy = body.meta.custom_data.triggered_by;
|
||||||
|
const subscriptionId = body.data.id;
|
||||||
const isSuccessful = body.data.attributes.status === 'active';
|
const isSuccessful = body.data.attributes.status === 'active';
|
||||||
|
|
||||||
console.log({
|
console.log({
|
||||||
orgId,
|
subscriptionId,
|
||||||
triggeredBy,
|
triggeredBy,
|
||||||
isSuccessful
|
isSuccessful
|
||||||
});
|
});
|
||||||
@@ -41,6 +42,7 @@ export async function POST({ request }) {
|
|||||||
if (isSuccessful && orgId) {
|
if (isSuccessful && orgId) {
|
||||||
const { data, error } = await createOrgPlan({
|
const { data, error } = await createOrgPlan({
|
||||||
supabase,
|
supabase,
|
||||||
|
subscriptionId,
|
||||||
orgId,
|
orgId,
|
||||||
triggeredBy: parseInt(triggeredBy),
|
triggeredBy: parseInt(triggeredBy),
|
||||||
planName: PLAN.EARLY_ADOPTER,
|
planName: PLAN.EARLY_ADOPTER,
|
||||||
@@ -57,10 +59,10 @@ export async function POST({ request }) {
|
|||||||
`${eventType}`
|
`${eventType}`
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
const orgId = body.meta.custom_data.org_id;
|
const subscriptionId = body.data.id;
|
||||||
const { data, error } = await cancelOrgPlan({
|
const { data, error } = await cancelOrgPlan({
|
||||||
orgId,
|
subscriptionId,
|
||||||
planName: PLAN.EARLY_ADOPTER
|
data: body.data
|
||||||
});
|
});
|
||||||
|
|
||||||
if (error) console.error('Error canceling org plan', error);
|
if (error) console.error('Error canceling org plan', error);
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { env } from '$env/dynamic/private';
|
||||||
|
import { Checkout } from 'polar-sveltekit';
|
||||||
|
|
||||||
|
export const GET = ({ ...params }) => {
|
||||||
|
const metadata = JSON.parse(params.url.searchParams.get('metadata') ?? '{}');
|
||||||
|
const successUrl = `/org/${metadata.orgSlug}?upgrade=true&confirmation=true`;
|
||||||
|
|
||||||
|
const request = Checkout({
|
||||||
|
accessToken: env.POLAR_ACCESS_TOKEN,
|
||||||
|
successUrl
|
||||||
|
});
|
||||||
|
|
||||||
|
return request(params);
|
||||||
|
};
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import { env } from '$env/dynamic/private';
|
||||||
|
import { getServerSupabase } from '$lib/utils/functions/supabase.server';
|
||||||
|
import { cancelOrgPlan, createOrgPlan, updateOrgPlan } from '$lib/utils/services/org';
|
||||||
|
import type { PolarWebhookPayload } from '$lib/utils/types/polar';
|
||||||
|
import { Webhooks } from 'polar-sveltekit';
|
||||||
|
import { PLAN } from 'shared/src/plans/constants';
|
||||||
|
|
||||||
|
export const POST = Webhooks({
|
||||||
|
webhookSecret: env.POLAR_WEBHOOK_SECRET!,
|
||||||
|
onPayload
|
||||||
|
});
|
||||||
|
|
||||||
|
async function onPayload(payload: PolarWebhookPayload) {
|
||||||
|
const supabase = getServerSupabase();
|
||||||
|
|
||||||
|
const metadata = payload.data.metadata;
|
||||||
|
const orgId = metadata.orgId;
|
||||||
|
const triggeredBy = metadata.triggeredBy;
|
||||||
|
const subscriptionId = payload.data.id;
|
||||||
|
const isSubscriptionActive = payload.data.status === 'active';
|
||||||
|
|
||||||
|
console.log({
|
||||||
|
orgId,
|
||||||
|
triggeredBy,
|
||||||
|
isSubscriptionActive,
|
||||||
|
subscriptionId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!orgId) {
|
||||||
|
console.error('Org ID is required');
|
||||||
|
throw new Error('Org ID is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (payload.type) {
|
||||||
|
case 'checkout.created':
|
||||||
|
break;
|
||||||
|
case 'checkout.updated':
|
||||||
|
break;
|
||||||
|
case 'subscription.created':
|
||||||
|
if (isSubscriptionActive) {
|
||||||
|
const { data, error } = await createOrgPlan({
|
||||||
|
supabase,
|
||||||
|
subscriptionId,
|
||||||
|
orgId,
|
||||||
|
triggeredBy: parseInt(triggeredBy),
|
||||||
|
planName: PLAN.EARLY_ADOPTER,
|
||||||
|
data: payload.data
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) throw new Error('Error creating org plan');
|
||||||
|
console.log('Subscription created', data);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'subscription.updated':
|
||||||
|
if (!isSubscriptionActive) {
|
||||||
|
// Handle Cancel Subscription
|
||||||
|
const { data, error } = await cancelOrgPlan({
|
||||||
|
subscriptionId,
|
||||||
|
data: payload.data
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) throw new Error('Error canceling org plan');
|
||||||
|
console.log('Subscription canceled', data);
|
||||||
|
} else {
|
||||||
|
// Handle Update Subscription
|
||||||
|
const { data, error } = await updateOrgPlan({
|
||||||
|
supabase,
|
||||||
|
subscriptionId,
|
||||||
|
data: payload.data
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) throw new Error('Error updating org plan');
|
||||||
|
console.log('Subscription updated', data);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'subscription.active':
|
||||||
|
break;
|
||||||
|
case 'subscription.revoked':
|
||||||
|
break;
|
||||||
|
case 'subscription.canceled':
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log('Unknown event', payload.type);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,8 +47,8 @@
|
|||||||
"DASHBOARD_LABEL": "Support us 😍",
|
"DASHBOARD_LABEL": "Support us 😍",
|
||||||
"DASHBOARD_LINK": "",
|
"DASHBOARD_LINK": "",
|
||||||
"IS_DISABLED": false,
|
"IS_DISABLED": false,
|
||||||
"PRODUCT_ID": "331121",
|
"PRODUCT_ID": "bb0c0593-3852-400a-a90a-f51fb35b51b7",
|
||||||
"PRODUCT_ID_YEARLY": "331122"
|
"PRODUCT_ID_YEARLY": "a84d3a82-3259-4300-b2bf-dccb7bd7814c"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ENTERPRISE": {
|
"ENTERPRISE": {
|
||||||
|
|||||||
Generated
+40
-1
@@ -364,6 +364,9 @@ importers:
|
|||||||
pluralize:
|
pluralize:
|
||||||
specifier: ^8.0.0
|
specifier: ^8.0.0
|
||||||
version: 8.0.0
|
version: 8.0.0
|
||||||
|
polar-sveltekit:
|
||||||
|
specifier: ^0.0.4
|
||||||
|
version: 0.0.4(@sveltejs/[email protected])([email protected])
|
||||||
posthog-js:
|
posthog-js:
|
||||||
specifier: ^1.180.1
|
specifier: ^1.180.1
|
||||||
version: 1.180.1
|
version: 1.180.1
|
||||||
@@ -4914,6 +4917,15 @@ packages:
|
|||||||
playwright: 1.48.2
|
playwright: 1.48.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@polar-sh/[email protected]([email protected]):
|
||||||
|
resolution: {integrity: sha512-IsIRJgDYB06pcoj9pnC+ovV3gW8cSUb/YJeMbU72puUe++fadxvsMsoL0+r59m/Cmghl79duILG6bQ6JOxYNDg==}
|
||||||
|
peerDependencies:
|
||||||
|
zod: '>= 3'
|
||||||
|
dependencies:
|
||||||
|
standardwebhooks: 1.0.0
|
||||||
|
zod: 3.21.4
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@polka/[email protected]:
|
/@polka/[email protected]:
|
||||||
resolution: {integrity: sha512-C16M+IYz0rgRhWZdCmK+h58JMv8vijAA61gmz2rspCSwKwzBebpdcsiUmwrtJRdphuY30i6BSLEOP8ppbNLyLg==}
|
resolution: {integrity: sha512-C16M+IYz0rgRhWZdCmK+h58JMv8vijAA61gmz2rspCSwKwzBebpdcsiUmwrtJRdphuY30i6BSLEOP8ppbNLyLg==}
|
||||||
|
|
||||||
@@ -6040,6 +6052,10 @@ packages:
|
|||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@stablelib/[email protected]:
|
||||||
|
resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@supabase/[email protected]:
|
/@supabase/[email protected]:
|
||||||
resolution: {integrity: sha512-BNzC5XhCzzCaggJ8s53DP+WeHHGT/NfTsx2wUSSGKR2/ikLFQTBCDzMvGz/PxYMqRko/LwncQtKXGOYp1PkPaw==}
|
resolution: {integrity: sha512-BNzC5XhCzzCaggJ8s53DP+WeHHGT/NfTsx2wUSSGKR2/ikLFQTBCDzMvGz/PxYMqRko/LwncQtKXGOYp1PkPaw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -6163,7 +6179,7 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@sveltejs/kit': ^2.0.0
|
'@sveltejs/kit': ^2.0.0
|
||||||
dependencies:
|
dependencies:
|
||||||
'@sveltejs/kit': 2.10.1(@sveltejs/vite-plugin-svelte@5.0.0)([email protected])(vite@6.0.0)
|
'@sveltejs/kit': 2.10.1(@sveltejs/vite-plugin-svelte@4.0.0)([email protected])(vite@5.4.11)
|
||||||
import-meta-resolve: 4.1.0
|
import-meta-resolve: 4.1.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
@@ -10778,6 +10794,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
|
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/[email protected]:
|
||||||
|
resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/[email protected]:
|
/[email protected]:
|
||||||
resolution: {integrity: sha512-B9/wizE4WngqQftFPmdaMYlXoJlJOYxGQOanC77fq9k8+Z0v5dDSVh+3glErdIROP//s/jgb7ZuxKfB8nVyo0g==}
|
resolution: {integrity: sha512-B9/wizE4WngqQftFPmdaMYlXoJlJOYxGQOanC77fq9k8+Z0v5dDSVh+3glErdIROP//s/jgb7ZuxKfB8nVyo0g==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -14962,6 +14982,18 @@ packages:
|
|||||||
engines: {node: '>=10.13.0'}
|
engines: {node: '>=10.13.0'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/[email protected](@sveltejs/[email protected])([email protected]):
|
||||||
|
resolution: {integrity: sha512-vlCbbBDJd8M6lqn6SfERSvH8xrozEGOJtpSFpygW/L1iJb8VpUEusymeD5IKdjZcqjNdhUaWh+Tbm0igYwTGoQ==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
peerDependencies:
|
||||||
|
'@sveltejs/kit': ^2.0.0
|
||||||
|
dependencies:
|
||||||
|
'@polar-sh/sdk': 0.20.0([email protected])
|
||||||
|
'@sveltejs/kit': 1.29.0([email protected])([email protected])
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- zod
|
||||||
|
dev: false
|
||||||
|
|
||||||
/[email protected]:
|
/[email protected]:
|
||||||
resolution: {integrity: sha512-la8gPM1WYeFznb/JqF4GiTkRRPZsfaj2+kCxqQgr2MJylMmIKUwBfWW8Wa5fml/8gmtlD5yI01MP1QCZPWmppA==}
|
resolution: {integrity: sha512-la8gPM1WYeFznb/JqF4GiTkRRPZsfaj2+kCxqQgr2MJylMmIKUwBfWW8Wa5fml/8gmtlD5yI01MP1QCZPWmppA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -16523,6 +16555,13 @@ packages:
|
|||||||
yargs: 16.2.0
|
yargs: 16.2.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/[email protected]:
|
||||||
|
resolution: {integrity: sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==}
|
||||||
|
dependencies:
|
||||||
|
'@stablelib/base64': 1.0.1
|
||||||
|
fast-sha256: 1.3.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/[email protected]:
|
/[email protected]:
|
||||||
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
|
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ create table "public"."organization_plan" (
|
|||||||
"is_active" boolean,
|
"is_active" boolean,
|
||||||
"deactivated_at" timestamp with time zone,
|
"deactivated_at" timestamp with time zone,
|
||||||
"updated_at" timestamp with time zone default now(),
|
"updated_at" timestamp with time zone default now(),
|
||||||
"lmz_data" jsonb,
|
"payload" jsonb,
|
||||||
"triggered_by" bigint
|
"triggered_by" bigint
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
alter table "public"."organization_plan" add column "provider" text default 'lmz'::text;
|
||||||
|
|
||||||
|
alter table "public"."organization_plan" add column "subscription_id" text not null;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX organization_plan_subscription_id_key ON public.organization_plan USING btree (subscription_id);
|
||||||
|
|
||||||
|
alter table "public"."organization_plan" add constraint "organization_plan_subscription_id_key" UNIQUE using index "organization_plan_subscription_id_key";
|
||||||
Reference in New Issue
Block a user