feat: dashboard billing (#193)
* feat: move plans into sharable json * remove unused code * update pnpm lock * update plans.json * implement early adopter pricing * docs: update meta title and description * Finished the UI for upgrading plans * Updated the pricing plans * Updated the cards ui * use relative import * improve styling of upgrade plan card * styling improvements, lemonsqueezy setup * trigger lemonsqueezy payment and add webhook * update pnpm lock * add logout route * feat: complete billing functionality * generate lock file * make crypto false on browser * feat: adding upgrade prompts to org * chore: add upgrade icon to upload video * feat: add upgrade prompt to certificate and settings * feat: implement onboarding setup page (#229) * working on the onboarding setup * working on the onboarding page * Almost done with the onboarding UI * finished the UI of the onBoarding setup * finished the UI of the onBoarding setup * Implementing the functions to carryout the logic of updating the UI onmount if a user has carried out a certain step or not * Implementing the functions to carryout the logic of updating the UI onmount if a user has carried out a certain step or not * making some chnages * finished the onBoarding task * updated some logic * Removed the uneeded svg from the static folder * fixed isCoursePublished not returning a boolean * improved the setup link protection * chore: make menuItems reactive * improve formatting * format supabase queries --------- Co-authored-by: Rotimi Best <best@classroomio.com> * revamp setup * improve the upgrade experience * migration * feat: add lemonsqueezy customer portal --------- Co-authored-by: emmanuel <chifez1@gmail.com>
This commit is contained in:
@@ -23,5 +23,6 @@ export const blockedSubdomain = [
|
||||
'academy',
|
||||
'schools',
|
||||
'training',
|
||||
'cloud'
|
||||
'cloud',
|
||||
'launchweek'
|
||||
];
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import posthog from 'posthog-js';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import { capturePosthogEvent } from '$lib/utils/services/posthog';
|
||||
import { currentOrg, orgs, defaultCurrentOrgState } from '$lib/utils/store/org';
|
||||
import { user, profile, defaultProfileState, defaultUserState } from '$lib/utils/store/user';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
export async function logout() {
|
||||
const { error } = await supabase.auth.signOut();
|
||||
|
||||
if (error) {
|
||||
console.error('Error logging out: ', error);
|
||||
}
|
||||
|
||||
currentOrg.set(defaultCurrentOrgState);
|
||||
orgs.set([]);
|
||||
user.set(defaultUserState);
|
||||
profile.set(defaultProfileState);
|
||||
|
||||
capturePosthogEvent('user_logged_out');
|
||||
posthog.reset();
|
||||
goto('/login');
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
export function genQuizPin(): number {
|
||||
var minm = 100000;
|
||||
var maxm = 999999;
|
||||
@@ -10,3 +12,13 @@ export function generateSitename(orgName: string): string {
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/[^a-zA-Z0-9-]/g, '');
|
||||
}
|
||||
|
||||
export function openUpgradeModal() {
|
||||
const url = new URL(window.location.href);
|
||||
|
||||
url.searchParams.set('upgrade', 'true');
|
||||
|
||||
const searchParams = url.searchParams.toString();
|
||||
|
||||
goto(window.location.pathname + `?${searchParams}`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { LEMON_SQUEEZY_API_KEY } from '$env/static/private';
|
||||
import { lemonSqueezySetup } from '@lemonsqueezy/lemonsqueezy.js';
|
||||
|
||||
export function configureLemonSqueezy() {
|
||||
return lemonSqueezySetup({
|
||||
apiKey: LEMON_SQUEEZY_API_KEY,
|
||||
onError: (error) => {
|
||||
throw new Error(`Lemon Squeezy API error: ${error.message}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import axios from 'axios';
|
||||
|
||||
interface SubscriptionData {
|
||||
productId: string;
|
||||
email: string;
|
||||
name: string;
|
||||
triggeredBy: string;
|
||||
orgId: string;
|
||||
}
|
||||
|
||||
interface SubscriptionResponse {
|
||||
success: boolean;
|
||||
checkoutURL: string;
|
||||
}
|
||||
|
||||
export async function subscribeToProduct(data: SubscriptionData): Promise<SubscriptionResponse> {
|
||||
const response = await axios({
|
||||
method: 'POST',
|
||||
url: '/api/lmz/subscribe',
|
||||
data
|
||||
});
|
||||
|
||||
return response.data;
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
import { goto } from '$app/navigation';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import { orgs, currentOrg, orgAudience, orgTeam } from '$lib/utils/store/org';
|
||||
import { ROLE, ROLE_LABEL } from '$lib/utils/constants/roles';
|
||||
import type { CurrentOrg, OrgTeamMember, OrgAudience } from '$lib/utils/types/org';
|
||||
import type { CurrentOrg, OrgTeamMember } from '$lib/utils/types/org';
|
||||
import type { OrganizationPlan } from '$lib/utils/types';
|
||||
|
||||
export async function getOrgTeam(orgId: string) {
|
||||
const { data, error } = await supabase
|
||||
@@ -79,7 +79,12 @@ export async function getOrganizations(userId: string) {
|
||||
avatar_url,
|
||||
landingpage,
|
||||
theme,
|
||||
created_at
|
||||
created_at,
|
||||
organization_plan(
|
||||
plan_name,
|
||||
is_active,
|
||||
subscriptionId:lmz_data->id
|
||||
)
|
||||
)
|
||||
`
|
||||
)
|
||||
@@ -108,7 +113,8 @@ export async function getOrganizations(userId: string) {
|
||||
avatar_url: orgMember?.organization?.avatar_url,
|
||||
memberId: orgMember?.id,
|
||||
role_id: orgMember?.role_id,
|
||||
landingpage: orgMember?.organization?.landingpage
|
||||
landingpage: orgMember?.organization?.landingpage,
|
||||
organization_plan: orgMember?.organization?.organization_plan
|
||||
});
|
||||
});
|
||||
|
||||
@@ -200,7 +206,6 @@ export async function getCourseBySiteName(siteName: string) {
|
||||
}
|
||||
|
||||
export async function getCurrentOrg(siteName: string, justGet = false) {
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('organization')
|
||||
.select(
|
||||
@@ -210,14 +215,17 @@ export async function getCurrentOrg(siteName: string, justGet = false) {
|
||||
siteName,
|
||||
avatar_url,
|
||||
landingpage,
|
||||
theme
|
||||
theme,
|
||||
organization_plan(
|
||||
plan_name,
|
||||
is_active
|
||||
)
|
||||
`
|
||||
)
|
||||
.eq('siteName', siteName)
|
||||
.returns<CurrentOrg[]>();
|
||||
console.log('data =', data);
|
||||
console.log('error =', error);
|
||||
|
||||
|
||||
const isDataEmpty = !data?.[0];
|
||||
|
||||
@@ -234,3 +242,32 @@ export async function getCurrentOrg(siteName: string, justGet = false) {
|
||||
return data[0];
|
||||
}
|
||||
}
|
||||
|
||||
export async function createOrgPlan(params: {
|
||||
orgId: string;
|
||||
planName: string;
|
||||
triggeredBy: number;
|
||||
data: OrganizationPlan['lmz_data'];
|
||||
}) {
|
||||
return await supabase.from('organization_plan').insert({
|
||||
activated_at: new Date().toDateString(),
|
||||
org_id: params.orgId,
|
||||
triggered_by: params.triggeredBy,
|
||||
plan_name: params.planName,
|
||||
is_active: true,
|
||||
lmz_data: params.data
|
||||
});
|
||||
}
|
||||
|
||||
export async function cancelOrgPlan(params: { orgId: string; planName: string }) {
|
||||
return await supabase
|
||||
.from('organization_plan')
|
||||
.update({
|
||||
is_active: false,
|
||||
deactivated_at: new Date().toDateString()
|
||||
})
|
||||
.match({
|
||||
plan_name: params.planName,
|
||||
org_id: params.orgId
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { Writable } from 'svelte/store';
|
||||
import type { CurrentOrg, OrgTeamMember, OrgAudience } from '../types/org';
|
||||
import { ROLE } from '$lib/utils/constants/roles';
|
||||
import type { UserLessonDataType } from '$lib/utils/types';
|
||||
import { PLAN } from 'shared-constants/src/plans/constants';
|
||||
|
||||
export const defaultCurrentOrgState: CurrentOrg = {
|
||||
id: '',
|
||||
@@ -15,7 +16,8 @@ export const defaultCurrentOrgState: CurrentOrg = {
|
||||
memberId: '',
|
||||
role_id: '',
|
||||
landingpage: {},
|
||||
theme: ''
|
||||
theme: '',
|
||||
organization_plan: []
|
||||
};
|
||||
|
||||
export const orgs = writable<CurrentOrg[]>([]);
|
||||
@@ -26,6 +28,9 @@ export const isOrgAdmin = derived(
|
||||
currentOrg,
|
||||
($currentOrg) => parseInt($currentOrg.role_id) === ROLE.ADMIN
|
||||
);
|
||||
export const currentOrgPlan = derived(currentOrg, ($currentOrg) =>
|
||||
$currentOrg.organization_plan.find((p) => p.is_active)
|
||||
);
|
||||
export const currentOrgPath = derived(currentOrg, ($currentOrg) =>
|
||||
$currentOrg.siteName ? `/org/${$currentOrg.siteName}` : ''
|
||||
);
|
||||
@@ -38,6 +43,21 @@ export const currentOrgDomain = derived(currentOrg, ($currentOrg) => {
|
||||
: '';
|
||||
});
|
||||
|
||||
// Utility org store
|
||||
export const isFreePlan = derived(
|
||||
currentOrgPlan,
|
||||
($plan) => !$plan || $plan.plan_name === PLAN.BASIC
|
||||
);
|
||||
export const currentOrgMaxAudience = derived(currentOrgPlan, ($plan) =>
|
||||
!$plan
|
||||
? 50
|
||||
: $plan.plan_name === PLAN.EARLY_ADOPTER
|
||||
? 10000
|
||||
: $plan.plan_name === PLAN.ENTERPRISE
|
||||
? Number.MAX_SAFE_INTEGER
|
||||
: 50
|
||||
);
|
||||
|
||||
// Quiz
|
||||
export const createQuizModal = writable({
|
||||
open: false,
|
||||
|
||||
@@ -105,6 +105,17 @@ export interface Organization {
|
||||
name: any; // type unknown;
|
||||
}
|
||||
|
||||
export interface OrganizationPlan {
|
||||
id: number;
|
||||
activated_at: string;
|
||||
deactivated_at: string;
|
||||
org_id: string;
|
||||
plan_name: string;
|
||||
is_active: boolean;
|
||||
updated_at: string;
|
||||
lmz_data: unknown;
|
||||
}
|
||||
|
||||
export interface Profile {
|
||||
id: string /* primary key */;
|
||||
fullname: string;
|
||||
|
||||
@@ -7,9 +7,14 @@ export interface CurrentOrg {
|
||||
memberId: string;
|
||||
role_id: string;
|
||||
landingpage: {
|
||||
[key: string]: any;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
theme: string;
|
||||
organization_plan: {
|
||||
subscriptionId: string;
|
||||
plan_name: string;
|
||||
is_active: boolean;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface OrgTeamMember {
|
||||
|
||||
Reference in New Issue
Block a user