diff --git a/src/lib/components/billing/alerts/missingPaymentMethod.svelte b/src/lib/components/billing/alerts/missingPaymentMethod.svelte new file mode 100644 index 000000000..b39fd7508 --- /dev/null +++ b/src/lib/components/billing/alerts/missingPaymentMethod.svelte @@ -0,0 +1,25 @@ + + +{#if ($orgMissingPaymentMethod.billingPlan === BillingPlan.PRO || $orgMissingPaymentMethod.billingPlan === BillingPlan.SCALE) && !$orgMissingPaymentMethod.paymentMethodId && !$orgMissingPaymentMethod.backupPaymentMethodId && !$page.url.pathname.includes('/console/account')} + + + Add a payment method to {$orgMissingPaymentMethod.name} to avoid service interruptions to + your projects. + + + + + +{/if} diff --git a/src/lib/sdk/billing.ts b/src/lib/sdk/billing.ts index 698f78377..66bab7a38 100644 --- a/src/lib/sdk/billing.ts +++ b/src/lib/sdk/billing.ts @@ -1,5 +1,5 @@ import type { Client, Models, Query } from '@appwrite.io/console'; -import type { Organization } from '../stores/organization'; +import type { Organization, OrganizationList } from '../stores/organization'; import type { PaymentMethod } from '@stripe/stripe-js'; import type { Tier } from '$lib/stores/billing'; @@ -274,6 +274,22 @@ export class Billing { this.client = client; } + async listOrganization(queries: Query[] = []): Promise { + const path = `/organizations`; + const params = { + queries + }; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call( + 'GET', + uri, + { + 'content-type': 'application/json' + }, + params + ); + } + async createOrganization( organizationId: string, name: string, diff --git a/src/lib/stores/billing.ts b/src/lib/stores/billing.ts index 22b873828..141b31f93 100644 --- a/src/lib/stores/billing.ts +++ b/src/lib/stores/billing.ts @@ -11,9 +11,10 @@ import PaymentAuthRequired from '$lib/components/billing/alerts/paymentAuthRequi import { addNotification, notifications } from './notifications'; import { goto } from '$app/navigation'; import { base } from '$app/paths'; -import { activeHeaderAlert } from '$routes/console/store'; +import { activeHeaderAlert, orgMissingPaymentMethod } from '$routes/console/store'; import MarkedForDeletion from '$lib/components/billing/alerts/markedForDeletion.svelte'; import { BillingPlan } from '$lib/constants'; +import MissingPaymentMethod from '$lib/components/billing/alerts/missingPaymentMethod.svelte'; export type Tier = 'tier-0' | 'tier-1' | 'tier-2'; @@ -265,3 +266,20 @@ export function checkForMarkedForDeletion(org: Organization) { }); } } + +export async function checkForMissingPaymentMethod() { + const orgs = await sdk.forConsole.billing.listOrganization([ + Query.notEqual('billingPlan', BillingPlan.STARTER), + Query.isNull('paymentMethodId'), + Query.isNull('backupPaymentMethodId') + ]); + if (orgs?.total) { + orgMissingPaymentMethod.set(orgs.teams[0]); + headerAlert.add({ + id: 'missingPaymentMethod', + component: MissingPaymentMethod, + show: true, + importance: 8 + }); + } +} diff --git a/src/lib/stores/organization.ts b/src/lib/stores/organization.ts index 38914e5b0..2beb13b03 100644 --- a/src/lib/stores/organization.ts +++ b/src/lib/stores/organization.ts @@ -22,6 +22,11 @@ export type Organization = Models.Team> & { billingPlanDowngrade?: string; }; +export type OrganizationList = { + teams: Organization[]; + total: number; +}; + export type BillingLimits = { bandwidth: number; documents: number; diff --git a/src/routes/console/+layout.svelte b/src/routes/console/+layout.svelte index 7bbd71997..d4c3d7dc2 100644 --- a/src/routes/console/+layout.svelte +++ b/src/routes/console/+layout.svelte @@ -19,7 +19,8 @@ checkPaymentAuthorizationRequired, calculateTrialDay, paymentExpired, - checkForMarkedForDeletion + checkForMarkedForDeletion, + checkForMissingPaymentMethod } from '$lib/stores/billing'; import { goto } from '$app/navigation'; import { CommandCenter, registerCommands, registerSearchers } from '$lib/commandCenter'; @@ -247,6 +248,7 @@ if (isCloud && hasStripePublicKey) { $stripe = await loadStripe(VARS.STRIPE_PUBLIC_KEY); + await checkForMissingPaymentMethod(); } }); diff --git a/src/routes/console/onboarding/+page.svelte b/src/routes/console/onboarding/+page.svelte index c4cda0b74..af2a1e2ba 100644 --- a/src/routes/console/onboarding/+page.svelte +++ b/src/routes/console/onboarding/+page.svelte @@ -25,16 +25,18 @@ let showCustomId = false; let plan: Tier; - const options = [ - { - value: BillingPlan.STARTER, - label: `Starter - ${formatCurrency($plansInfo.get(BillingPlan.STARTER).price)}/month` - }, - { - value: BillingPlan.PRO, - label: `Pro - ${formatCurrency($plansInfo.get(BillingPlan.PRO).price)}/month + add-ons` - } - ]; + const options = isCloud + ? [ + { + value: BillingPlan.STARTER, + label: `Starter - ${formatCurrency($plansInfo.get(BillingPlan.STARTER).price)}/month` + }, + { + value: BillingPlan.PRO, + label: `Pro - ${formatCurrency($plansInfo.get(BillingPlan.PRO).price)}/month + add-ons` + } + ] + : []; onMount(() => { if (isCloud) { diff --git a/src/routes/console/store.ts b/src/routes/console/store.ts index 3dabf7951..662572c6f 100644 --- a/src/routes/console/store.ts +++ b/src/routes/console/store.ts @@ -1,5 +1,6 @@ import { page } from '$app/stores'; import type { HeaderAlert } from '$lib/stores/headerAlert'; +import type { Organization } from '$lib/stores/organization'; import type { Models } from '@appwrite.io/console'; import { derived, writable } from 'svelte/store'; @@ -10,3 +11,4 @@ export const consoleVariables = derived( ); export const activeHeaderAlert = writable(null); +export const orgMissingPaymentMethod = writable(null);