Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
51 changes: 51 additions & 0 deletions src/lib/stores/billing.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
2 changes: 2 additions & 0 deletions src/lib/stores/organization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
9 changes: 8 additions & 1 deletion src/lib/stores/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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 };
4 changes: 3 additions & 1 deletion src/routes/console/organization-[organization]/+layout.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
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';

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) {
Expand Down
104 changes: 102 additions & 2 deletions src/routes/console/organization-[organization]/settings/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -61,6 +139,28 @@
</CardGrid>
</Form>

<Form on:submit={savePaymentDetails}>
<CardGrid>
<Heading tag="h6" size="7">Create Payment Method</Heading>
{#each $paymentMethods.paymentMethods as paymentMethod}
<p>
{paymentMethod.brand}
{paymentMethod.last4}, expiring: {paymentMethod.expiryMonth}/{paymentMethod.expiryYear}
{#if paymentMethod.default} <span class="icon-check-circle" /> {/if}
</p>
{/each}

<div id="payment-element">
<!-- Elements will create form elements here -->
</div>

<svelte:fragment slot="actions">
<Button submit>Save</Button>
<Button on:click={createPaymentMethod}>Create Payment Method</Button>
</svelte:fragment>
</CardGrid>
</Form>

<CardGrid danger>
<div>
<Heading tag="h6" size="7">Delete Organization</Heading>
Expand Down
45 changes: 44 additions & 1 deletion src/routes/console/project-[project]/settings/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
import { sdkForConsole } from '$lib/stores/sdk';
import { cloudSdk, sdkForConsole } from '$lib/stores/sdk';
import { onMount } from 'svelte';
import { toLocaleDateTime } from '$lib/helpers/date';
import { addNotification } from '$lib/stores/notifications';
Expand All @@ -16,13 +16,15 @@
import { trackEvent } from '$lib/actions/analytics';

let name: string = null;
let plan: string = null;
let showDelete = false;
let updating = false;
const endpoint = sdkForConsole.client.config.endpoint;
const projectId = $page.params.project;

onMount(async () => {
name ??= $project.name;
plan ??= $project.billingPlan;
});

async function updateName() {
Expand All @@ -43,9 +45,28 @@
}
}

async function updatePlan() {
updating = true;
try {
await cloudSdk.billing.updateProjectPlan($project.$id, plan);
invalidate(Dependencies.PROJECT);
addNotification({
type: 'success',
message: 'Project plan has been updated'
});
trackEvent('submit_project_update_plan');
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
}
}

$: {
// When project name is updated, finalize the updating flow
$project.name;
$project.billingPlan;
updating = false;
}

Expand Down Expand Up @@ -118,6 +139,28 @@
</CardGrid>
</Form>

<Form on:submit={updatePlan}>
<CardGrid>
<Heading tag="h6" size="7">Update Plan</Heading>

<svelte:fragment slot="aside">
<FormList>
<InputText
id="plan"
label="plan"
bind:value={plan}
required
placeholder="Select plan" />
</FormList>
</svelte:fragment>

<svelte:fragment slot="actions">
<Button disabled={plan === $project.billingPlan || updating} submit
>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>

<CardGrid>
<Heading tag="h6" size="7">Services</Heading>
<p class="text">Choose services you wish to enable or disable.</p>
Expand Down