diff --git a/package-lock.json b/package-lock.json index 071882cfc9..19dd58fdb0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@popperjs/core": "^2.11.6", "@sentry/svelte": "^7.36.0", "@sentry/tracing": "^7.36.0", + "@stripe/stripe-js": "^1.46.0", "analytics": "^0.8.1", "echarts": "^5.4.1", "pretty-bytes": "^6.1.0", @@ -1927,6 +1928,11 @@ "@sinonjs/commons": "^2.0.0" } }, + "node_modules/@stripe/stripe-js": { + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-1.46.0.tgz", + "integrity": "sha512-dkm0zCEoRLu5rTnsIgwDf/QG2DKcalOT2dk1IVgMySOHWTChLyOvQwMYhEduGgLvyYWTwNhAUV4WOLPQvjwLwA==" + }, "node_modules/@sveltejs/adapter-static": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-1.0.6.tgz", diff --git a/package.json b/package.json index 02996ed909..24a5109034 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "@popperjs/core": "^2.11.6", "@sentry/svelte": "^7.36.0", "@sentry/tracing": "^7.36.0", + "@stripe/stripe-js": "^1.46.0", "analytics": "^0.8.1", "echarts": "^5.4.1", "pretty-bytes": "^6.1.0", diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 428ae63923..7ba07a8aac 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -10,6 +10,7 @@ export enum Mode { export const growthEndpoint = import.meta.env.VITE_APPWRITE_GROWTH_ENDPOINT; export enum Dependencies { + PAYMENT_METHODS = 'dependency:paymentMethods', ORGANIZATION = 'dependency:organization', PROJECT = 'dependency:project', PROJECTS = 'dependency:projects', diff --git a/src/lib/stores/billing.ts b/src/lib/stores/billing.ts new file mode 100644 index 0000000000..f358686308 --- /dev/null +++ b/src/lib/stores/billing.ts @@ -0,0 +1,51 @@ +import type { Client, Payload } from '@aw-labs/appwrite-console'; + +export class Billing { + client: Client; + + constructor(client: Client) { + this.client = client; + } + + async listPaymentMethods(teamId: string) { + const path = `/teams/${teamId}/payment-methods`; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call('GET', uri); + } + + async createPaymentMethod(teamId: string) { + const path = `/teams/${teamId}/payment-methods`; + const params = {}; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call( + 'POST', + uri, + { + 'content-type': 'application/json' + }, + params + ); + } + + async updatePaymentMethod(teamId: string, paymentMethodId: string, providerMethodId: string) { + const path = `/teams/${teamId}/payment-methods/${paymentMethodId}`; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call( + 'PUT', + uri, + { 'content-type': 'application/json' }, + { + providerMethodId + } + ); + } + + async updateProjectPlan(projectId: string, billingPlan: string) { + const path = `/project/${projectId}/plan`; + const params: Payload = { + billingPlan + }; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call('patch', uri, { 'content-type': 'application/json' }, params); + } +} diff --git a/src/lib/stores/organization.ts b/src/lib/stores/organization.ts index 0c780f1d85..4dbf3fb227 100644 --- a/src/lib/stores/organization.ts +++ b/src/lib/stores/organization.ts @@ -9,4 +9,6 @@ export const organizationList = derived( ($page) => $page.data.organizations as Models.TeamList ); export const organization = derived(page, ($page) => $page.data.organization as Models.Team); + +export const paymentMethods = derived(page, ($page) => $page.data.paymentMethods); export const members = derived(page, ($page) => $page.data.members as Models.MembershipList); diff --git a/src/lib/stores/sdk.ts b/src/lib/stores/sdk.ts index cffc23deca..870cade528 100644 --- a/src/lib/stores/sdk.ts +++ b/src/lib/stores/sdk.ts @@ -12,6 +12,8 @@ import { Users } from '@aw-labs/appwrite-console'; +import { Billing } from './billing'; + const endpoint = import.meta.env.VITE_APPWRITE_ENDPOINT?.toString() ?? `${window?.location?.origin}/v1`; const clientConsole = new Client(); @@ -48,4 +50,9 @@ const sdkForProject = { users: new Users(clientProject) }; -export { sdkForConsole, sdkForProject, setProject }; +const cloudSdk = { + ...sdkForConsole, + billing: new Billing(clientConsole) +}; + +export { sdkForConsole, sdkForProject, cloudSdk, setProject }; diff --git a/src/routes/console/organization-[organization]/+layout.ts b/src/routes/console/organization-[organization]/+layout.ts index 97de8b900d..9d6522015c 100644 --- a/src/routes/console/organization-[organization]/+layout.ts +++ b/src/routes/console/organization-[organization]/+layout.ts @@ -1,6 +1,6 @@ import Header from './header.svelte'; import Breadcrumbs from './breadcrumbs.svelte'; -import { sdkForConsole } from '$lib/stores/sdk'; +import { cloudSdk, sdkForConsole } from '$lib/stores/sdk'; import type { LayoutLoad } from './$types'; import { error } from '@sveltejs/kit'; import { Dependencies } from '$lib/constants'; @@ -8,12 +8,14 @@ import { Dependencies } from '$lib/constants'; export const load: LayoutLoad = async ({ params, parent, depends }) => { await parent(); depends(Dependencies.ORGANIZATION); + depends(Dependencies.PAYMENT_METHODS); try { return { header: Header, breadcrumbs: Breadcrumbs, organization: await sdkForConsole.teams.get(params.organization), + paymentMethods: await cloudSdk.billing.listPaymentMethods(params.organization), members: await sdkForConsole.teams.listMemberships(params.organization) }; } catch (e) { diff --git a/src/routes/console/organization-[organization]/settings/+page.svelte b/src/routes/console/organization-[organization]/settings/+page.svelte index 13aa5d1c5f..9e0eb9b5e9 100644 --- a/src/routes/console/organization-[organization]/settings/+page.svelte +++ b/src/routes/console/organization-[organization]/settings/+page.svelte @@ -3,21 +3,99 @@ import { InputText, Form, Button } from '$lib/elements/forms'; import { Container } from '$lib/layout'; import { addNotification } from '$lib/stores/notifications'; - import { sdkForConsole } from '$lib/stores/sdk'; - import { members, organization } from '$lib/stores/organization'; + import { cloudSdk, sdkForConsole } from '$lib/stores/sdk'; + import { members, organization, paymentMethods } from '$lib/stores/organization'; import { invalidate } from '$app/navigation'; import { Dependencies } from '$lib/constants'; import { onMount } from 'svelte'; import Delete from '../deleteOrganization.svelte'; import { trackEvent } from '$lib/actions/analytics'; + import { loadStripe } from '@stripe/stripe-js'; let name: string; let showDelete = false; + let stripe: any; + let elements: any; + let paymentMethod: any; onMount(() => { name = $organization.name; + initStripe(); }); + async function initStripe() { + stripe = await loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY?.toString()); + } + + async function createPaymentMethod(event) { + event.preventDefault(); + + try { + paymentMethod = await cloudSdk.billing.createPaymentMethod($organization.$id); + const options = { + clientSecret: paymentMethod.clientSecret, + // Fully customizable with appearance API. + appearance: { + /*...*/ + } + }; + // Set up Stripe.js and Elements to use in checkout form, passing the client secret obtained in step 3 + elements = stripe.elements(options); + + // Create and mount the Payment Element + const paymentElement = elements.create('payment'); + paymentElement.mount('#payment-element'); + } catch (error) { + addNotification({ + message: error.toString(), + type: 'error' + }); + } + } + + async function savePaymentDetails(event) { + event.preventDefault(); + + const { error } = await stripe.confirmSetup({ + //`Elements` instance that was used to create the Payment Element + elements, + confirmParams: { + return_url: 'http://localhost:3000' + }, + redirect: 'if_required' + }); + + if (error) { + addNotification({ + message: error.message, + type: 'error' + }); + } else { + if (paymentMethod) { + const { error, setupIntent } = await stripe.retrieveSetupIntent( + paymentMethod.clientSecret + ); + + if (error) { + addNotification({ + message: error.message, + type: 'error' + }); + } else if (setupIntent && setupIntent.status === 'succeeded') { + //update payment method + await cloudSdk.billing.updatePaymentMethod( + $organization.$id, + paymentMethod.$id, + setupIntent.payment_method + ); + const paymentElement = elements.getElement('payment'); + await invalidate(Dependencies.PAYMENT_METHODS); + paymentElement.destroy(); + } + } + } + } + async function updateName() { try { await sdkForConsole.teams.update($organization.$id, name); @@ -61,6 +139,28 @@ +
+ + Create Payment Method + {#each $paymentMethods.paymentMethods as paymentMethod} +

+ {paymentMethod.brand} + {paymentMethod.last4}, expiring: {paymentMethod.expiryMonth}/{paymentMethod.expiryYear} + {#if paymentMethod.default} {/if} +

+ {/each} + +
+ +
+ + + + + +
+
+
Delete Organization diff --git a/src/routes/console/project-[project]/settings/+page.svelte b/src/routes/console/project-[project]/settings/+page.svelte index 76277fbf24..c8cfc0d863 100644 --- a/src/routes/console/project-[project]/settings/+page.svelte +++ b/src/routes/console/project-[project]/settings/+page.svelte @@ -1,5 +1,5 @@