mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge branch 'billing-sdk-refactor' into 'remove-billing-plan'.
This commit is contained in:
+1
-1
@@ -22,7 +22,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/svelte": "^1.1.24",
|
||||
"@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@5b9d902",
|
||||
"@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@d7e354b",
|
||||
"@appwrite.io/pink-icons": "0.25.0",
|
||||
"@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@865e2fc",
|
||||
"@appwrite.io/pink-legacy": "^1.0.3",
|
||||
|
||||
Generated
+5
-5
@@ -12,8 +12,8 @@ importers:
|
||||
specifier: ^1.1.24
|
||||
version: 1.1.24(svelte@5.25.3)(zod@3.24.3)
|
||||
'@appwrite.io/console':
|
||||
specifier: https://pkg.vc/-/@appwrite/@appwrite.io/console@5b9d902
|
||||
version: https://pkg.vc/-/@appwrite/@appwrite.io/console@5b9d902
|
||||
specifier: https://pkg.vc/-/@appwrite/@appwrite.io/console@d7e354b
|
||||
version: https://pkg.vc/-/@appwrite/@appwrite.io/console@d7e354b
|
||||
'@appwrite.io/pink-icons':
|
||||
specifier: 0.25.0
|
||||
version: 0.25.0
|
||||
@@ -272,8 +272,8 @@ packages:
|
||||
'@analytics/type-utils@0.6.2':
|
||||
resolution: {integrity: sha512-TD+xbmsBLyYy/IxFimW/YL/9L2IEnM7/EoV9Aeh56U64Ify8o27HJcKjo38XY9Tcn0uOq1AX3thkKgvtWvwFQg==}
|
||||
|
||||
'@appwrite.io/console@https://pkg.vc/-/@appwrite/@appwrite.io/console@5b9d902':
|
||||
resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/console@5b9d902}
|
||||
'@appwrite.io/console@https://pkg.vc/-/@appwrite/@appwrite.io/console@d7e354b':
|
||||
resolution: {tarball: https://pkg.vc/-/@appwrite/@appwrite.io/console@d7e354b}
|
||||
version: 1.10.0
|
||||
|
||||
'@appwrite.io/pink-icons-svelte@2.0.0-RC.1':
|
||||
@@ -3823,7 +3823,7 @@ snapshots:
|
||||
|
||||
'@analytics/type-utils@0.6.2': {}
|
||||
|
||||
'@appwrite.io/console@https://pkg.vc/-/@appwrite/@appwrite.io/console@5b9d902': {}
|
||||
'@appwrite.io/console@https://pkg.vc/-/@appwrite/@appwrite.io/console@d7e354b': {}
|
||||
|
||||
'@appwrite.io/pink-icons-svelte@2.0.0-RC.1(svelte@5.25.3)':
|
||||
dependencies:
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { Card, Divider, Layout, Typography } from '@appwrite.io/pink-svelte';
|
||||
import { CreditsApplied } from '.';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { AppwriteException, BillingPlan, type Models } from '@appwrite.io/console';
|
||||
import { AppwriteException, type Models } from '@appwrite.io/console';
|
||||
import DiscountsApplied from './discountsApplied.svelte';
|
||||
|
||||
export let billingPlan: Models.BillingPlan;
|
||||
@@ -23,12 +23,9 @@
|
||||
collaborators: string[],
|
||||
couponId: string | undefined
|
||||
) {
|
||||
/* TODO: @itznotabug - temporary fix */
|
||||
const billingPlanTyped = billingPlan as BillingPlan;
|
||||
|
||||
try {
|
||||
estimation = await sdk.forConsole.organizations.estimationCreateOrganization({
|
||||
billingPlan: billingPlanTyped,
|
||||
billingPlan,
|
||||
invites: collaborators ?? [],
|
||||
couponId: couponId === '' ? null : couponId
|
||||
});
|
||||
@@ -59,7 +56,7 @@
|
||||
try {
|
||||
estimation = await sdk.forConsole.organizations.estimationUpdatePlan({
|
||||
organizationId,
|
||||
billingPlan: billingPlan as BillingPlan,
|
||||
billingPlan: billingPlan,
|
||||
invites: collaborators ?? [],
|
||||
couponId: couponId && couponId.length > 0 ? couponId : null
|
||||
});
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
<section class="progressbar__container">
|
||||
{#each $$props.data as item}
|
||||
<Tooltip disabled={!item.tooltip}>
|
||||
<Tooltip disabled={!item.tooltip} maxWidth="max-content">
|
||||
<div
|
||||
class="progressbar__content"
|
||||
style:background-color={item.color}
|
||||
|
||||
+1
-87
@@ -1,5 +1,4 @@
|
||||
import type { Client, Models } from '@appwrite.io/console';
|
||||
import type { OrganizationError } from '../stores/organization';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
export type AllowedRegions =
|
||||
| 'fra'
|
||||
@@ -14,88 +13,3 @@ export type AllowedRegions =
|
||||
| 'default'; //TODO: remove after migration
|
||||
|
||||
export type BillingPlansMap = Map<string, Models.BillingPlan>;
|
||||
|
||||
export class Billing {
|
||||
client: Client;
|
||||
|
||||
constructor(client: Client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
async createOrganization(
|
||||
organizationId: string,
|
||||
name: string,
|
||||
billingPlan: string,
|
||||
paymentMethodId: string,
|
||||
billingAddressId: string = undefined,
|
||||
couponId: string = null,
|
||||
invites: Array<string> = [],
|
||||
budget: number = undefined,
|
||||
taxId: string = null
|
||||
): Promise<Models.Organization | OrganizationError> {
|
||||
const path = `/organizations`;
|
||||
const params = {
|
||||
organizationId,
|
||||
name,
|
||||
billingPlan,
|
||||
paymentMethodId,
|
||||
billingAddressId,
|
||||
couponId,
|
||||
invites,
|
||||
budget,
|
||||
taxId
|
||||
};
|
||||
const uri = new URL(this.client.config.endpoint + path);
|
||||
return await this.client.call(
|
||||
'POST',
|
||||
uri,
|
||||
{
|
||||
'content-type': 'application/json'
|
||||
},
|
||||
params
|
||||
);
|
||||
}
|
||||
|
||||
async updatePlan(
|
||||
organizationId: string,
|
||||
billingPlan: string,
|
||||
paymentMethodId: string,
|
||||
billingAddressId: string = undefined,
|
||||
couponId: string = null,
|
||||
invites: Array<string> = [],
|
||||
budget: number = undefined,
|
||||
taxId: string = null
|
||||
): Promise<Models.Organization | OrganizationError> {
|
||||
const path = `/organizations/${organizationId}/plan`;
|
||||
const params = {
|
||||
organizationId,
|
||||
billingPlan,
|
||||
paymentMethodId,
|
||||
billingAddressId,
|
||||
couponId,
|
||||
invites,
|
||||
budget,
|
||||
taxId
|
||||
};
|
||||
|
||||
const uri = new URL(this.client.config.endpoint + path);
|
||||
return await this.client.call(
|
||||
'patch',
|
||||
uri,
|
||||
{
|
||||
'content-type': 'application/json'
|
||||
},
|
||||
params
|
||||
);
|
||||
}
|
||||
|
||||
async cancelDowngrade(
|
||||
organizationId: string
|
||||
): Promise<Models.Organization | OrganizationError> {
|
||||
const path = `/organizations/${organizationId}/plan/cancel`;
|
||||
const uri = new URL(this.client.config.endpoint + path);
|
||||
return await this.client.call('patch', uri, {
|
||||
'content-type': 'application/json'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import PaymentAuthRequired from '$lib/components/billing/alerts/paymentAuthRequi
|
||||
import PaymentMandate from '$lib/components/billing/alerts/paymentMandate.svelte';
|
||||
import { NEW_DEV_PRO_UPGRADE_COUPON } from '$lib/constants';
|
||||
import { cachedStore } from '$lib/helpers/cache';
|
||||
import { type Size, sizeToBytes } from '$lib/helpers/sizeConvertion';
|
||||
import type { BillingPlansMap } from '$lib/sdk/billing';
|
||||
import { isCloud } from '$lib/system';
|
||||
import { activeHeaderAlert, orgMissingPaymentMethod } from '$routes/(console)/store';
|
||||
@@ -25,7 +24,7 @@ import {
|
||||
import { derived, get, writable } from 'svelte/store';
|
||||
import { headerAlert } from './headerAlert';
|
||||
import { addNotification, notifications } from './notifications';
|
||||
import { currentPlan, type OrganizationError } from './organization';
|
||||
import { currentPlan } from './organization';
|
||||
import { canSeeBilling } from './roles';
|
||||
import { sdk } from './sdk';
|
||||
import { user } from './user';
|
||||
@@ -667,31 +666,10 @@ export const upgradeURL = derived(page, ($page) => {
|
||||
});
|
||||
});
|
||||
|
||||
export const billingURL = derived(page, ($page) => {
|
||||
return resolve('/(console)/organization-[organization]/billing', {
|
||||
organization: $page.data?.organization?.$id
|
||||
});
|
||||
});
|
||||
|
||||
export const hideBillingHeaderRoutes = [resolve('/account'), resolve('/create-organization')];
|
||||
|
||||
export function calculateExcess(addon: Models.AggregationTeam, plan: Models.BillingPlan) {
|
||||
return {
|
||||
bandwidth: calculateResourceSurplus(addon.usageBandwidth, plan.bandwidth),
|
||||
storage: calculateResourceSurplus(addon.usageStorage, plan.storage, 'GB'),
|
||||
executions: calculateResourceSurplus(addon.usageExecutions, plan.executions, 'GB'),
|
||||
members: addon.additionalMembers
|
||||
};
|
||||
}
|
||||
|
||||
export function calculateResourceSurplus(total: number, limit: number, limitUnit: Size = null) {
|
||||
if (total === undefined || limit === undefined) return 0;
|
||||
const realLimit = (limitUnit ? sizeToBytes(limit, limitUnit) : limit) || Infinity;
|
||||
return total > realLimit ? total - realLimit : 0;
|
||||
}
|
||||
|
||||
export function isOrganization(
|
||||
org: Models.Organization | OrganizationError
|
||||
): org is Models.Organization {
|
||||
return (org as Models.Organization).$id !== undefined;
|
||||
export function isPaymentAuthenticationRequired(
|
||||
org: Models.Organization | Models.PaymentAuthentication
|
||||
): org is Models.PaymentAuthentication {
|
||||
return 'clientSecret' in org;
|
||||
}
|
||||
|
||||
@@ -4,15 +4,6 @@ import { isCloud } from '$lib/system';
|
||||
import { derived, writable } from 'svelte/store';
|
||||
import { type Models, Platform, Query } from '@appwrite.io/console';
|
||||
|
||||
export type OrganizationError = {
|
||||
status: number;
|
||||
message: string;
|
||||
teamId: string;
|
||||
invoiceId: string;
|
||||
clientSecret: string;
|
||||
type: string;
|
||||
};
|
||||
|
||||
export const newOrgModal = writable<boolean>(false);
|
||||
export const newMemberModal = writable<boolean>(false);
|
||||
export const organizationList = derived(
|
||||
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
Realtime,
|
||||
Organizations
|
||||
} from '@appwrite.io/console';
|
||||
import { Billing } from '../sdk/billing';
|
||||
import { Backups } from '../sdk/backups';
|
||||
import { Sources } from '$lib/sdk/sources';
|
||||
import {
|
||||
@@ -91,7 +90,6 @@ function createConsoleSdk(client: Client) {
|
||||
migrations: new Migrations(client),
|
||||
console: new Console(client),
|
||||
assistant: new Assistant(client),
|
||||
billing: new Billing(client),
|
||||
sources: new Sources(client),
|
||||
sites: new Sites(client),
|
||||
domains: new Domains(client),
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import { toLocaleDate } from '$lib/helpers/date';
|
||||
import { Wizard } from '$lib/layout';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { type OrganizationError, organizationList } from '$lib/stores/organization';
|
||||
import { organizationList } from '$lib/stores/organization';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { confirmPayment } from '$lib/stores/stripe.js';
|
||||
import { BillingPlanGroup, ID, type Models } from '@appwrite.io/console';
|
||||
@@ -18,7 +18,7 @@
|
||||
import {
|
||||
billingIdToPlan,
|
||||
getBasePlanFromGroup,
|
||||
isOrganization,
|
||||
isPaymentAuthenticationRequired,
|
||||
plansInfo
|
||||
} from '$lib/stores/billing';
|
||||
import { Fieldset, Icon, Layout, Tooltip } from '@appwrite.io/pink-svelte';
|
||||
@@ -133,32 +133,30 @@
|
||||
if (couponForm && !couponForm.checkValidity()) return;
|
||||
isSubmitting.set(true);
|
||||
try {
|
||||
let org: Models.Organization | OrganizationError;
|
||||
let org: Models.Organization | Models.PaymentAuthentication;
|
||||
// Create new org
|
||||
if (selectedOrgId === newOrgId) {
|
||||
org = await sdk.forConsole.billing.createOrganization(
|
||||
newOrgId,
|
||||
org = await sdk.forConsole.organizations.create({
|
||||
organizationId: newOrgId,
|
||||
name,
|
||||
billingPlan.$id,
|
||||
billingPlan: billingPlan.$id,
|
||||
paymentMethodId,
|
||||
undefined,
|
||||
couponData.code ? couponData.code : null,
|
||||
collaborators,
|
||||
billingBudget,
|
||||
taxId
|
||||
);
|
||||
invites: collaborators,
|
||||
couponId: couponData.code ? couponData.code : null,
|
||||
taxId,
|
||||
budget: billingBudget
|
||||
});
|
||||
}
|
||||
|
||||
// Upgrade existing org
|
||||
else if (selectedOrg?.billingPlan !== billingPlan.$id && isUpgrade()) {
|
||||
org = await sdk.forConsole.billing.updatePlan(
|
||||
selectedOrg.$id,
|
||||
billingPlan.$id,
|
||||
org = await sdk.forConsole.organizations.updatePlan({
|
||||
organizationId: selectedOrg.$id,
|
||||
billingPlan: billingPlan.$id,
|
||||
paymentMethodId,
|
||||
undefined,
|
||||
couponData.code ? couponData.code : null,
|
||||
collaborators
|
||||
);
|
||||
invites: collaborators,
|
||||
couponId: couponData.code ? couponData.code : null
|
||||
});
|
||||
}
|
||||
// Existing pro org, apply credits
|
||||
else {
|
||||
@@ -169,11 +167,11 @@
|
||||
});
|
||||
}
|
||||
|
||||
if (!isOrganization(org) && org.status === 402) {
|
||||
if (isPaymentAuthenticationRequired(org)) {
|
||||
let clientSecret = org.clientSecret;
|
||||
let params = new URLSearchParams();
|
||||
params.append('type', 'payment_confirmed');
|
||||
params.append('org', org.teamId);
|
||||
params.append('org', org.organizationId);
|
||||
for (const [key, value] of page.url.searchParams.entries()) {
|
||||
if (key !== 'type' && key !== 'id') {
|
||||
params.append(key, value);
|
||||
@@ -188,12 +186,12 @@
|
||||
);
|
||||
|
||||
org = await sdk.forConsole.organizations.validatePayment({
|
||||
organizationId: org.teamId,
|
||||
organizationId: org.organizationId,
|
||||
invites: collaborators
|
||||
});
|
||||
}
|
||||
|
||||
if (isOrganization(org)) {
|
||||
if (!isPaymentAuthenticationRequired(org)) {
|
||||
trackEvent(Submit.CreditRedeem, {
|
||||
coupon: couponData.code,
|
||||
campaign: couponData?.campaign
|
||||
|
||||
@@ -8,12 +8,15 @@
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Button, Form, InputTags, InputText } from '$lib/elements/forms';
|
||||
import { Wizard } from '$lib/layout';
|
||||
import { billingIdToPlan, getBasePlanFromGroup, isOrganization } from '$lib/stores/billing';
|
||||
import {
|
||||
billingIdToPlan,
|
||||
getBasePlanFromGroup,
|
||||
isPaymentAuthenticationRequired
|
||||
} from '$lib/stores/billing';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import type { OrganizationError } from '$lib/stores/organization';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { confirmPayment } from '$lib/stores/stripe';
|
||||
import { BillingPlan, BillingPlanGroup, ID, type Models } from '@appwrite.io/console';
|
||||
import { BillingPlanGroup, ID, type Models } from '@appwrite.io/console';
|
||||
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
|
||||
import { Divider, Fieldset, Icon, Layout, Link, Typography } from '@appwrite.io/pink-svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
@@ -24,8 +27,8 @@
|
||||
const { data }: PageProps = $props();
|
||||
|
||||
let showExitModal = $state(false);
|
||||
let selectedPlan = $state(data.plan);
|
||||
let previousPage: string = $state(resolve('/(console)'));
|
||||
let selectedPlan: Models.BillingPlan = $state(data.plan);
|
||||
let selectedCoupon: Partial<Models.Coupon> | null = $state(data.coupon);
|
||||
|
||||
let isSubmitting = $state(writable(false));
|
||||
@@ -64,8 +67,8 @@
|
||||
}
|
||||
|
||||
if (page.url.searchParams.has('plan')) {
|
||||
const plan = page.url.searchParams.get('plan') as BillingPlan;
|
||||
if (plan && Object.values(BillingPlan).includes(plan)) {
|
||||
const plan = page.url.searchParams.get('plan');
|
||||
if (plan) {
|
||||
billingPlan = plan;
|
||||
}
|
||||
}
|
||||
@@ -94,7 +97,7 @@
|
||||
invites
|
||||
});
|
||||
|
||||
if (isOrganization(org)) {
|
||||
if (!isPaymentAuthenticationRequired(org)) {
|
||||
await preloadData(`${base}/console/organization-${org.$id}`);
|
||||
await goto(`${base}/console/organization-${org.$id}`);
|
||||
addNotification({
|
||||
@@ -113,33 +116,31 @@
|
||||
|
||||
async function create() {
|
||||
try {
|
||||
let org: Models.Organization | OrganizationError;
|
||||
let org: Models.Organization | Models.PaymentAuthentication;
|
||||
|
||||
if (selectedPlan.group === BillingPlanGroup.Starter) {
|
||||
org = await sdk.forConsole.billing.createOrganization(
|
||||
ID.unique(),
|
||||
name,
|
||||
getBasePlanFromGroup(BillingPlanGroup.Starter).$id,
|
||||
null
|
||||
);
|
||||
org = await sdk.forConsole.organizations.create({
|
||||
organizationId: ID.unique(),
|
||||
name: name,
|
||||
billingPlan: getBasePlanFromGroup(BillingPlanGroup.Starter).$id
|
||||
});
|
||||
} else {
|
||||
org = await sdk.forConsole.billing.createOrganization(
|
||||
ID.unique(),
|
||||
org = await sdk.forConsole.organizations.create({
|
||||
organizationId: ID.unique(),
|
||||
name,
|
||||
selectedPlan.$id,
|
||||
billingPlan: selectedPlan.$id,
|
||||
paymentMethodId,
|
||||
undefined,
|
||||
selectedCoupon?.code,
|
||||
collaborators,
|
||||
billingBudget,
|
||||
couponId: selectedCoupon?.code,
|
||||
invites: collaborators,
|
||||
budget: billingBudget,
|
||||
taxId
|
||||
);
|
||||
});
|
||||
|
||||
if (!isOrganization(org) && org.status === 402) {
|
||||
if (isPaymentAuthenticationRequired(org)) {
|
||||
let clientSecret = org.clientSecret;
|
||||
let params = new URLSearchParams();
|
||||
params.append('type', 'payment_confirmed');
|
||||
params.append('id', org.teamId);
|
||||
params.append('id', org.organizationId);
|
||||
for (const [key, value] of page.url.searchParams.entries()) {
|
||||
if (key !== 'type' && key !== 'id') {
|
||||
params.append(key, value);
|
||||
@@ -152,7 +153,7 @@
|
||||
paymentMethodId,
|
||||
`${base}/create-organization?${params}`
|
||||
);
|
||||
await validate(org.teamId, collaborators);
|
||||
await validate(org.organizationId, collaborators);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,7 +163,7 @@
|
||||
members_invited: collaborators?.length
|
||||
});
|
||||
|
||||
if (isOrganization(org)) {
|
||||
if (!isPaymentAuthenticationRequired(org)) {
|
||||
await invalidate(Dependencies.ACCOUNT);
|
||||
await preloadData(`${base}/organization-${org.$id}`);
|
||||
await goto(`${base}/organization-${org.$id}`);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import type { PageLoad } from './$types';
|
||||
import { BillingPlan, BillingPlanGroup, type Models, Platform } from '@appwrite.io/console';
|
||||
import { BillingPlanGroup, type Models, Platform } from '@appwrite.io/console';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { billingIdToPlan, getNextTierBillingPlan } from '$lib/stores/billing';
|
||||
|
||||
@@ -44,7 +44,8 @@ async function getPlanFromUrl(url: URL): Promise<Models.BillingPlan | null> {
|
||||
}
|
||||
|
||||
// fallback
|
||||
return await sdk.forConsole.console.getPlan({ planId: BillingPlan.Tier0 });
|
||||
// @todo: avoid hardcoded maybe
|
||||
return await sdk.forConsole.console.getPlan({ planId: 'tier-0' });
|
||||
}
|
||||
|
||||
function getPlanFromCache(plan: string): Models.BillingPlan | null {
|
||||
|
||||
@@ -22,12 +22,11 @@
|
||||
try {
|
||||
if (isCloud) {
|
||||
const starter = getBasePlanFromGroup(BillingPlanGroup.Starter);
|
||||
organization = await sdk.forConsole.billing.createOrganization(
|
||||
ID.unique(),
|
||||
organizationName,
|
||||
starter.$id,
|
||||
null
|
||||
);
|
||||
organization = await sdk.forConsole.organizations.create({
|
||||
organizationId: ID.unique(),
|
||||
name: organizationName,
|
||||
billingPlan: starter.$id
|
||||
});
|
||||
|
||||
trackEvent(Submit.OrganizationCreate, {
|
||||
plan: starter?.name,
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { PageLoad } from './$types';
|
||||
import { isCloud } from '$lib/system';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { getBasePlanFromGroup, isOrganization } from '$lib/stores/billing';
|
||||
import { getBasePlanFromGroup, isPaymentAuthenticationRequired } from '$lib/stores/billing';
|
||||
import { BillingPlanGroup, ID, type Models, Query } from '@appwrite.io/console';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { base } from '$app/paths';
|
||||
@@ -25,19 +25,18 @@ export const load: PageLoad = async ({ parent }) => {
|
||||
try {
|
||||
if (isCloud) {
|
||||
const starterPlan = getBasePlanFromGroup(BillingPlanGroup.Starter);
|
||||
const org = await sdk.forConsole.billing.createOrganization(
|
||||
ID.unique(),
|
||||
'Personal projects',
|
||||
starterPlan.$id,
|
||||
null
|
||||
);
|
||||
const org = await sdk.forConsole.organizations.create({
|
||||
organizationId: ID.unique(),
|
||||
name: 'Personal projects',
|
||||
billingPlan: starterPlan.$id
|
||||
});
|
||||
trackEvent(Submit.OrganizationCreate, {
|
||||
plan: starterPlan?.name,
|
||||
budget_cap_enabled: false,
|
||||
members_invited: 0
|
||||
});
|
||||
|
||||
if (isOrganization(org)) {
|
||||
if (!isPaymentAuthenticationRequired(org)) {
|
||||
return {
|
||||
accountPrefs,
|
||||
organization: org
|
||||
|
||||
+4
-1
@@ -15,7 +15,10 @@
|
||||
|
||||
async function cancelDowngrade() {
|
||||
try {
|
||||
await sdk.forConsole.billing.cancelDowngrade($organization.$id);
|
||||
await sdk.forConsole.organizations.cancelDowngrade({
|
||||
organizationId: $organization.$id
|
||||
});
|
||||
|
||||
await invalidate(Dependencies.ORGANIZATION);
|
||||
showCancel = false;
|
||||
addNotification({
|
||||
|
||||
@@ -9,7 +9,11 @@
|
||||
import { Button, Form, InputSelect, InputTags, InputTextarea } from '$lib/elements/forms';
|
||||
import { formatCurrency } from '$lib/helpers/numbers.js';
|
||||
import { Wizard } from '$lib/layout';
|
||||
import { billingIdToPlan, getBasePlanFromGroup, isOrganization } from '$lib/stores/billing';
|
||||
import {
|
||||
billingIdToPlan,
|
||||
getBasePlanFromGroup,
|
||||
isPaymentAuthenticationRequired
|
||||
} from '$lib/stores/billing';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { currentPlan, organization } from '$lib/stores/organization';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
@@ -161,11 +165,11 @@
|
||||
async function downgrade() {
|
||||
try {
|
||||
// 1) update the plan first
|
||||
await sdk.forConsole.billing.updatePlan(
|
||||
data.organization.$id,
|
||||
selectedPlan.$id,
|
||||
await sdk.forConsole.organizations.updatePlan({
|
||||
organizationId: data.organization.$id,
|
||||
billingPlan: selectedPlan.$id,
|
||||
paymentMethodId
|
||||
);
|
||||
});
|
||||
|
||||
// 2) If the target plan has a project limit, apply selected projects now
|
||||
const targetProjectsLimit = selectedPlan?.projects ?? 0;
|
||||
@@ -211,7 +215,7 @@
|
||||
invites
|
||||
});
|
||||
|
||||
if (isOrganization(org)) {
|
||||
if (!isPaymentAuthenticationRequired(org)) {
|
||||
await invalidate(Dependencies.ACCOUNT);
|
||||
await invalidate(Dependencies.ORGANIZATION);
|
||||
|
||||
@@ -244,18 +248,17 @@
|
||||
!data?.members?.memberships?.find((m) => m.userEmail === collaborator)
|
||||
);
|
||||
}
|
||||
const org = await sdk.forConsole.billing.updatePlan(
|
||||
data.organization.$id,
|
||||
selectedPlan.$id,
|
||||
const org = await sdk.forConsole.organizations.updatePlan({
|
||||
organizationId: data.organization.$id,
|
||||
billingPlan: selectedPlan.$id,
|
||||
paymentMethodId,
|
||||
undefined,
|
||||
selectedCoupon?.code,
|
||||
newCollaborators,
|
||||
billingBudget,
|
||||
taxId ? taxId : null
|
||||
);
|
||||
couponId: selectedCoupon?.code,
|
||||
invites: newCollaborators,
|
||||
budget: billingBudget,
|
||||
taxId: taxId ? taxId : null
|
||||
});
|
||||
|
||||
if (!isOrganization(org) && org.status == 402) {
|
||||
if (isPaymentAuthenticationRequired(org)) {
|
||||
let clientSecret = org.clientSecret;
|
||||
let params = new URLSearchParams();
|
||||
for (const [key, value] of page.url.searchParams.entries()) {
|
||||
@@ -264,7 +267,7 @@
|
||||
}
|
||||
}
|
||||
params.append('type', 'payment_confirmed');
|
||||
params.append('id', org.teamId);
|
||||
params.append('id', org.organizationId);
|
||||
params.append('invites', collaborators.join(','));
|
||||
params.append('plan', selectedPlan.$id);
|
||||
await confirmPayment(
|
||||
@@ -273,10 +276,10 @@
|
||||
paymentMethodId,
|
||||
base + '/change-plan?' + params.toString()
|
||||
);
|
||||
await validate(org.teamId, collaborators);
|
||||
await validate(org.organizationId, collaborators);
|
||||
}
|
||||
|
||||
if (isOrganization(org)) {
|
||||
if (!isPaymentAuthenticationRequired(org)) {
|
||||
/**
|
||||
* Reload on upgrade (e.g. Free → Paid)
|
||||
*/
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
import PlatformFlutterImgSourceDark from './assets/platform-flutter-dark.svg';
|
||||
import PlatformSdkImgSource from './assets/platform-sdk.jpg';
|
||||
import PlatformSdkImgSourceDark from './assets/platform-sdk-dark.png';
|
||||
import { base } from '$app/paths';
|
||||
import { resolve } from '$app/paths';
|
||||
import { isSmallViewport } from '$lib/stores/viewport';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { getPlatformInfo } from '$lib/helpers/platform';
|
||||
@@ -36,40 +36,45 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
|
||||
export let pingCount = 0;
|
||||
export let platforms: Models.Platform[] = [];
|
||||
let {
|
||||
pingCount = 0,
|
||||
platforms = []
|
||||
}: {
|
||||
pingCount: number;
|
||||
platforms: Array<Models.Platform>;
|
||||
} = $props();
|
||||
|
||||
const platformMap = $derived.by(() => {
|
||||
const map = new Map<string, Models.Platform>();
|
||||
platforms.forEach((platform) => {
|
||||
const platformInfo = getPlatformInfo(platform.type);
|
||||
map.set(platformInfo.name, platform);
|
||||
});
|
||||
|
||||
return map;
|
||||
});
|
||||
|
||||
const projectRoute = $derived.by(() => {
|
||||
return resolve('/(console)/project-[region]-[project]', {
|
||||
region: page.params.region,
|
||||
project: page.params.project
|
||||
});
|
||||
});
|
||||
|
||||
function createKey() {
|
||||
trackEvent(Click.KeyCreateClick, {
|
||||
source: 'onboarding'
|
||||
});
|
||||
goto(
|
||||
`${base}/project-${page.params.region}-${page.params.project}/overview/api-keys/create`,
|
||||
{
|
||||
replaceState: true
|
||||
}
|
||||
);
|
||||
trackEvent(Click.KeyCreateClick, { source: 'onboarding' });
|
||||
|
||||
goto(`${projectRoute}/overview/api-keys/create`, { replaceState: true });
|
||||
}
|
||||
|
||||
function openPlatformWizard(type: number, platform?: Models.Platform) {
|
||||
if (platform) {
|
||||
continuePlatform(type, platform.name, platform.key, platform.type);
|
||||
continuePlatform(type, platform.name, platform.type);
|
||||
} else {
|
||||
trackEvent(Click.PlatformCreateClick, { source: 'onboarding' });
|
||||
addPlatform(type);
|
||||
}
|
||||
}
|
||||
|
||||
let platformMap = new Map();
|
||||
|
||||
$: {
|
||||
let updatedMap = new Map();
|
||||
platforms.forEach((platform) => {
|
||||
const platformInfo = getPlatformInfo(platform.type);
|
||||
updatedMap.set(platformInfo.name, platform);
|
||||
});
|
||||
platformMap = updatedMap;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div style:container-type="inline-size">
|
||||
@@ -410,9 +415,7 @@
|
||||
<Card.Button
|
||||
on:click={() => {
|
||||
trackEvent(Click.OnboardingSetupDatabaseClick);
|
||||
goto(
|
||||
`${base}/project-${page.params.region}-${page.params.project}/databases`
|
||||
);
|
||||
goto(`${projectRoute}/databases`);
|
||||
}}
|
||||
padding="s"
|
||||
><Layout.Stack gap="xl"
|
||||
@@ -507,7 +510,7 @@
|
||||
justifyContent="flex-end">
|
||||
<Link.Anchor
|
||||
variant="quiet-muted"
|
||||
href={`${base}/project-${page.params.region}-${page.params.project}/auth/settings`}
|
||||
href={`${projectRoute}/auth/settings`}
|
||||
on:click={() => {
|
||||
trackEvent(
|
||||
Click.OnboardingAuthEmailPasswordClick
|
||||
@@ -517,7 +520,7 @@
|
||||
</Link.Anchor>
|
||||
<Link.Anchor
|
||||
variant="quiet-muted"
|
||||
href={`${base}/project-${page.params.region}-${page.params.project}/auth/settings`}
|
||||
href={`${projectRoute}/auth/settings`}
|
||||
on:click={() => {
|
||||
trackEvent(
|
||||
Click.OnboardingAuthOauth2Click
|
||||
@@ -525,7 +528,7 @@
|
||||
}}>OAuth 2</Link.Anchor>
|
||||
<Link.Anchor
|
||||
variant="quiet-muted"
|
||||
href={`${base}/project-${page.params.region}-${page.params.project}/auth/settings`}
|
||||
href={`${projectRoute}/auth/settings`}
|
||||
on:click={() => {
|
||||
trackEvent(
|
||||
Click.OnboardingAuthAllMethodsClick
|
||||
|
||||
@@ -27,25 +27,20 @@
|
||||
});
|
||||
}
|
||||
|
||||
export async function continuePlatform(
|
||||
platform: Platform,
|
||||
name: string,
|
||||
key: string,
|
||||
type: PlatformType
|
||||
) {
|
||||
export async function continuePlatform(platform: Platform, name: string, type: PlatformType) {
|
||||
createPlatform.set({
|
||||
name: name,
|
||||
key: key,
|
||||
type: type
|
||||
});
|
||||
|
||||
trackEvent(Click.PlatformCreateClick, {
|
||||
platform: platforms[platform],
|
||||
state: 'continue'
|
||||
});
|
||||
|
||||
wizard.start(platforms[platform], null, 1, {
|
||||
isConnectPlatform: true,
|
||||
platform: type,
|
||||
key: key
|
||||
platform: type
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ function createPlatformStore() {
|
||||
$id: null,
|
||||
name: null,
|
||||
hostname: null,
|
||||
key: null,
|
||||
store: null,
|
||||
type: null
|
||||
});
|
||||
@@ -20,7 +19,6 @@ function createPlatformStore() {
|
||||
$id: null,
|
||||
name: null,
|
||||
hostname: null,
|
||||
key: null,
|
||||
store: null,
|
||||
type: null
|
||||
});
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { Wizard } from '$lib/layout';
|
||||
import { Icon, Input, Layout, Popover, Tag, Typography, Card } from '@appwrite.io/pink-svelte';
|
||||
import {
|
||||
Icon,
|
||||
Input,
|
||||
Layout,
|
||||
Popover,
|
||||
Tag,
|
||||
Typography,
|
||||
Card,
|
||||
Upload
|
||||
} from '@appwrite.io/pink-svelte';
|
||||
import { supportData, isSupportOnline } from './wizard/support/store';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
@@ -25,8 +34,10 @@
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import { VARS } from '$lib/system';
|
||||
import { IconCheckCircle, IconXCircle, IconInfo } from '@appwrite.io/pink-icons-svelte';
|
||||
import { removeFile } from '$lib/helpers/files';
|
||||
|
||||
let projectOptions = $state<Array<{ value: string; label: string }>>([]);
|
||||
let files = $state<FileList | null>(null);
|
||||
|
||||
// Category options with display names
|
||||
const categories = [
|
||||
@@ -118,25 +129,29 @@
|
||||
? `${$supportData.category}-${$supportData.topic}`.toLowerCase()
|
||||
: $supportData.category.toLowerCase();
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('email', $user.email);
|
||||
formData.append('subject', $supportData.subject);
|
||||
formData.append('firstName', ($user?.name || 'Unknown').slice(0, 40));
|
||||
formData.append('message', $supportData.message);
|
||||
formData.append('tags[]', categoryTopicTag);
|
||||
formData.append(
|
||||
'customFields',
|
||||
JSON.stringify([
|
||||
{ id: '41612', value: $supportData.category },
|
||||
{ id: '48492', value: $organization?.$id ?? '' },
|
||||
{ id: '48491', value: $supportData?.project ?? '' },
|
||||
{ id: '56023', value: $supportData?.severity ?? '' },
|
||||
{ id: '56024', value: $organization?.billingPlan ?? '' }
|
||||
])
|
||||
);
|
||||
if (files && files.length > 0) {
|
||||
formData.append('attachment', files[0]);
|
||||
}
|
||||
|
||||
const response = await fetch(`${VARS.GROWTH_ENDPOINT}/support`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: $user.email,
|
||||
subject: $supportData.subject,
|
||||
firstName: ($user?.name || 'Unknown').slice(0, 40),
|
||||
message: $supportData.message,
|
||||
tags: [categoryTopicTag],
|
||||
customFields: [
|
||||
{ id: '41612', value: $supportData.category },
|
||||
{ id: '48492', value: $organization?.$id ?? '' },
|
||||
{ id: '48491', value: $supportData?.project ?? '' },
|
||||
{ id: '56023', value: $supportData?.severity ?? '' },
|
||||
{ id: '56024', value: $organization?.billingPlan ?? '' }
|
||||
]
|
||||
})
|
||||
body: formData
|
||||
});
|
||||
trackEvent(Submit.SupportTicket);
|
||||
if (response.status !== 200) {
|
||||
@@ -171,6 +186,13 @@
|
||||
|
||||
$wizard.finalAction = handleSubmit;
|
||||
|
||||
function handleInvalid(_e: CustomEvent) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: 'Invalid file'
|
||||
});
|
||||
}
|
||||
|
||||
const workTimings = {
|
||||
start: '16:00',
|
||||
end: '00:00',
|
||||
@@ -281,6 +303,26 @@
|
||||
label="Tell us a bit more"
|
||||
required
|
||||
maxlength={4096} />
|
||||
<Upload.Dropzone bind:files on:invalid={handleInvalid} maxSize={5 * 1024 * 1024}>
|
||||
<Layout.Stack alignItems="center" gap="s">
|
||||
<Typography.Text variant="l-500"
|
||||
>Drag and drop a file here or click to upload</Typography.Text>
|
||||
<Typography.Caption variant="400">Max file size: 5MB</Typography.Caption>
|
||||
</Layout.Stack>
|
||||
</Upload.Dropzone>
|
||||
{#if files}
|
||||
<Upload.List
|
||||
files={Array.from(files).map((f) => {
|
||||
return {
|
||||
...f,
|
||||
name: f.name,
|
||||
size: f.size,
|
||||
extension: f.type,
|
||||
removable: true
|
||||
};
|
||||
})}
|
||||
on:remove={(e) => (files = removeFile(e.detail, files))} />
|
||||
{/if}
|
||||
<Layout.Stack direction="row" justifyContent="flex-end" gap="s">
|
||||
<Button
|
||||
size="s"
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import CustomId from '$lib/components/customId.svelte';
|
||||
import { Button, Form, InputSelect } from '$lib/elements/forms';
|
||||
import type { AllowedRegions } from '$lib/sdk/billing.js';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { isCloud } from '$lib/system';
|
||||
@@ -24,6 +23,7 @@
|
||||
import { filterRegions } from '$lib/helpers/regions';
|
||||
import { loadAvailableRegions } from '$routes/(console)/regions';
|
||||
import { regions as regionsStore } from '$lib/stores/organization';
|
||||
import type { AllowedRegions } from '$lib/sdk/billing';
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ export const load: PageLoad = async ({ parent, url }) => {
|
||||
if (!organizations?.total) {
|
||||
try {
|
||||
if (isCloud) {
|
||||
await sdk.forConsole.billing.createOrganization(
|
||||
await sdk.forConsole.organizations.create(
|
||||
ID.unique(),
|
||||
'Personal Projects',
|
||||
getBasePlanFromGroup(BillingPlanGroup.Starter).$id,
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
import CustomId from '$lib/components/customId.svelte';
|
||||
import { SvgIcon } from '$lib/components/index.js';
|
||||
import { Button, Form, InputSelect } from '$lib/elements/forms';
|
||||
import type { AllowedRegions } from '$lib/sdk/billing.js';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { getFrameworkIcon } from '$lib/stores/sites.js';
|
||||
@@ -28,6 +27,7 @@
|
||||
import { filterRegions } from '$lib/helpers/regions';
|
||||
import { loadAvailableRegions } from '$routes/(console)/regions';
|
||||
import { regions as regionsStore } from '$lib/stores/organization';
|
||||
import type { AllowedRegions } from '$lib/sdk/billing';
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
|
||||
@@ -85,12 +85,11 @@ export const load: PageLoad = async ({ parent, url }) => {
|
||||
if (!organizations?.total) {
|
||||
try {
|
||||
if (isCloud) {
|
||||
await sdk.forConsole.billing.createOrganization(
|
||||
ID.unique(),
|
||||
'Personal Projects',
|
||||
getBasePlanFromGroup(BillingPlanGroup.Starter).$id,
|
||||
null
|
||||
);
|
||||
await sdk.forConsole.organizations.create({
|
||||
organizationId: ID.unique(),
|
||||
name: 'Personal Projects',
|
||||
billingPlan: getBasePlanFromGroup(BillingPlanGroup.Starter).$id
|
||||
});
|
||||
} else {
|
||||
await sdk.forConsole.teams.create({
|
||||
teamId: ID.unique(),
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
import CustomId from '$lib/components/customId.svelte';
|
||||
import { SvgIcon } from '$lib/components/index.js';
|
||||
import { Button, Form, InputSelect } from '$lib/elements/forms';
|
||||
import type { AllowedRegions } from '$lib/sdk/billing.js';
|
||||
import { app } from '$lib/stores/app';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
@@ -23,6 +22,7 @@
|
||||
Typography
|
||||
} from '@appwrite.io/pink-svelte';
|
||||
import { filterRegions } from '$lib/helpers/regions';
|
||||
import type { AllowedRegions } from '$lib/sdk/billing';
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
|
||||
@@ -40,12 +40,11 @@ export const load = async ({ parent, url, params }) => {
|
||||
const organizations = account?.$id ? await getTeamOrOrganizationList() : undefined;
|
||||
|
||||
if (!organizations?.total && account?.$id) {
|
||||
await sdk.forConsole.billing.createOrganization(
|
||||
ID.unique(),
|
||||
'Personal project',
|
||||
getBasePlanFromGroup(BillingPlanGroup.Starter).$id,
|
||||
null
|
||||
);
|
||||
await sdk.forConsole.organizations.create({
|
||||
organizationId: ID.unique(),
|
||||
name: 'Personal project',
|
||||
billingPlan: getBasePlanFromGroup(BillingPlanGroup.Starter).$id
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user