mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
feat: new billing onboarding flow
This commit is contained in:
1 parent
bf2969614d
commit
ca24b0b82e
7 files changed
+170
-55
No files matched your search
@@ -2,6 +2,7 @@
|
||||
import { FormList, InputText } from '$lib/elements/forms';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { CreditCardBrandImage, RadioBoxes } from '..';
|
||||
import { unmountPaymentElement } from '$lib/stores/stripe';
|
||||
|
||||
export let methods: Record<string, unknown>[];
|
||||
export let group: string;
|
||||
@@ -33,6 +34,7 @@
|
||||
|
||||
onDestroy(() => {
|
||||
observer.disconnect();
|
||||
unmountPaymentElement();
|
||||
});
|
||||
|
||||
$: if (element) {
|
||||
|
||||
@@ -122,6 +122,7 @@
|
||||
} else {
|
||||
$wizard.step--;
|
||||
}
|
||||
wizard.setInterceptor(null);
|
||||
trackEvent('wizard_back');
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,15 @@ export async function initializeStripe() {
|
||||
paymentElement.mount('#payment-element');
|
||||
}
|
||||
|
||||
// TODO: fix redirect
|
||||
export async function unmountPaymentElement() {
|
||||
isStripeInitialized.set(false);
|
||||
if (paymentElement) {
|
||||
paymentElement.unmount();
|
||||
}
|
||||
clientSecret = null;
|
||||
paymentMethod = null;
|
||||
elements = null;
|
||||
}
|
||||
|
||||
export async function submitStripeCard(name: string, urlRoute?: string) {
|
||||
try {
|
||||
|
||||
@@ -31,14 +31,18 @@ function createWizardStore() {
|
||||
return {
|
||||
subscribe,
|
||||
set,
|
||||
start: (component: typeof SvelteComponent<unknown>, media: string = null) =>
|
||||
start: (
|
||||
component: typeof SvelteComponent<unknown>,
|
||||
media: string = null,
|
||||
step: number = 1
|
||||
) =>
|
||||
update((n) => {
|
||||
n.show = true;
|
||||
n.component = component;
|
||||
n.interceptor = null;
|
||||
n.interceptorNotificationEnabled = true;
|
||||
n.media = media;
|
||||
n.step = 1;
|
||||
n.step = step;
|
||||
n.cover = null;
|
||||
n.nextDisabled = false;
|
||||
n.finalAction = null;
|
||||
|
||||
@@ -28,6 +28,23 @@
|
||||
|
||||
async function create() {
|
||||
try {
|
||||
// Create free organization if coming from onboarding
|
||||
if ($page.url.pathname.includes('/console/onboarding')) {
|
||||
const freeOrg = await sdk.forConsole.billing.createOrganization(
|
||||
ID.unique(),
|
||||
'Personal Projects',
|
||||
BillingPlan.STARTER,
|
||||
null,
|
||||
null
|
||||
);
|
||||
await sdk.forConsole.projects.create(
|
||||
ID.unique(),
|
||||
'My first project',
|
||||
freeOrg.$id,
|
||||
'fra'
|
||||
);
|
||||
}
|
||||
|
||||
const org = await sdk.forConsole.billing.createOrganization(
|
||||
$createOrganization.id ?? ID.unique(),
|
||||
$createOrganization.name,
|
||||
@@ -64,6 +81,25 @@
|
||||
await sdk.forConsole.billing.updateTaxId(org.$id, $createOrganization.taxId);
|
||||
}
|
||||
|
||||
trackEvent(Submit.OrganizationCreate, {
|
||||
customId: !!$createOrganization.id,
|
||||
plan: tierToPlan($createOrganization.billingPlan)?.name,
|
||||
budget_cap_enabled: !!$createOrganization?.billingBudget,
|
||||
members_invited: $createOrganization?.collaborators?.length
|
||||
});
|
||||
|
||||
//Create first pro project if onboarding
|
||||
if ($page.url.pathname.includes('/console/onboarding')) {
|
||||
await sdk.forConsole.projects.create(
|
||||
ID.unique(),
|
||||
'My first Pro project',
|
||||
org.$id,
|
||||
'fra'
|
||||
);
|
||||
|
||||
trackEvent(Submit.ProjectCreate);
|
||||
}
|
||||
|
||||
await invalidate(Dependencies.ACCOUNT);
|
||||
await preloadData(`/console/organization-${org.$id}`);
|
||||
await goto(`/console/organization-${org.$id}`);
|
||||
@@ -71,12 +107,7 @@
|
||||
type: 'success',
|
||||
message: `${$createOrganization.name ?? 'Organization'} has been created`
|
||||
});
|
||||
trackEvent(Submit.OrganizationCreate, {
|
||||
customId: !!$createOrganization.id,
|
||||
plan: tierToPlan($createOrganization.billingPlan)?.name,
|
||||
budget_cap_enabled: !!$createOrganization?.billingBudget,
|
||||
members_invited: $createOrganization?.collaborators?.length
|
||||
});
|
||||
|
||||
wizard.hide();
|
||||
if (org.billingPlan === BillingPlan.PRO) {
|
||||
wizard.showCover(HoodieCover);
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
import { goto, invalidate } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import { Card } from '$lib/components';
|
||||
import { Card, Heading } from '$lib/components';
|
||||
import CustomId from '$lib/components/customId.svelte';
|
||||
import { BillingPlan, Dependencies } from '$lib/constants';
|
||||
import { Pill } from '$lib/elements';
|
||||
import { Button, Form, InputText } from '$lib/elements/forms';
|
||||
import { Button, Form, InputSelect, InputText } from '$lib/elements/forms';
|
||||
import FormList from '$lib/elements/forms/formList.svelte';
|
||||
import { Container } from '$lib/layout';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
@@ -16,10 +16,18 @@
|
||||
import { ID } from '@appwrite.io/console';
|
||||
import { onMount } from 'svelte';
|
||||
import CreateOrganizationCloud from '../createOrganizationCloud.svelte';
|
||||
import { tierToPlan, type Tier } from '$lib/stores/billing';
|
||||
import { createOrganization } from '../wizard/cloudOrganization/store';
|
||||
|
||||
let name: string;
|
||||
let id: string;
|
||||
let showCustomId = false;
|
||||
let plan: Tier;
|
||||
|
||||
const options = [
|
||||
{ value: BillingPlan.STARTER, label: 'Starter - $0/billing period' },
|
||||
{ value: BillingPlan.PRO, label: 'Pro - $15/billing period + add-ons' }
|
||||
];
|
||||
|
||||
onMount(() => {
|
||||
if (isCloud) {
|
||||
@@ -32,66 +40,124 @@
|
||||
}
|
||||
});
|
||||
|
||||
async function createProject() {
|
||||
try {
|
||||
const org = await createOrganization();
|
||||
const project = await sdk.forConsole.projects.create(
|
||||
id ?? ID.unique(),
|
||||
name,
|
||||
org.$id,
|
||||
isCloud ? 'fra' : 'default'
|
||||
);
|
||||
await invalidate(Dependencies.ACCOUNT);
|
||||
goto(`/console/project-${project.$id}`);
|
||||
trackEvent(Submit.ProjectCreate, {
|
||||
customId: !!id,
|
||||
teamId: org.$id
|
||||
});
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
message: error.message,
|
||||
type: 'error'
|
||||
});
|
||||
trackError(error, Submit.ProjectCreate);
|
||||
}
|
||||
}
|
||||
|
||||
async function createOrganization() {
|
||||
async function handleSubmit() {
|
||||
if (isCloud) {
|
||||
return await sdk.forConsole.billing.createOrganization(
|
||||
ID.unique(),
|
||||
'Personal Projects',
|
||||
BillingPlan.STARTER,
|
||||
null,
|
||||
null
|
||||
);
|
||||
} else return await sdk.forConsole.teams.create(ID.unique(), 'Personal Projects');
|
||||
if (plan === BillingPlan.STARTER) {
|
||||
try {
|
||||
const org = await sdk.forConsole.billing.createOrganization(
|
||||
id ?? ID.unique(),
|
||||
name ?? 'Personal Projects',
|
||||
plan,
|
||||
null,
|
||||
null
|
||||
);
|
||||
trackEvent(Submit.OrganizationCreate, {
|
||||
customId: !!id,
|
||||
plan: tierToPlan(plan)?.name
|
||||
});
|
||||
await sdk.forConsole.projects.create(
|
||||
ID.unique(),
|
||||
'My first project',
|
||||
org.$id,
|
||||
'fra'
|
||||
);
|
||||
await invalidate(Dependencies.ACCOUNT);
|
||||
await goto(`/console/organization-${org.$id}`);
|
||||
addNotification({
|
||||
message: `${
|
||||
name?.length ? name : 'Personal Projects'
|
||||
} organization successfully created`,
|
||||
type: 'success'
|
||||
});
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
message: error.message,
|
||||
type: 'error'
|
||||
});
|
||||
trackError(error, Submit.OrganizationCreate);
|
||||
}
|
||||
} else {
|
||||
wizard.start(CreateOrganizationCloud, null, 2);
|
||||
$createOrganization.name = name?.length ? name : 'Personal Projects';
|
||||
$createOrganization.billingPlan = plan;
|
||||
$createOrganization.id = id;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const org = await sdk.forConsole.teams.create(
|
||||
id ?? ID.unique(),
|
||||
name?.length ? name : 'Personal Projects'
|
||||
);
|
||||
const project = await sdk.forConsole.projects.create(
|
||||
ID.unique(),
|
||||
'My first project',
|
||||
org.$id,
|
||||
'default'
|
||||
);
|
||||
await invalidate(Dependencies.ACCOUNT);
|
||||
await goto(`/console/project-${project.$id}`);
|
||||
addNotification({
|
||||
message: `${name ?? 'Personal Projects'} organization successfully created`,
|
||||
type: 'success'
|
||||
});
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
message: error.message,
|
||||
type: 'error'
|
||||
});
|
||||
trackError(error, Submit.OrganizationCreate);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Container overlapCover size="large">
|
||||
<Card>
|
||||
<Form onSubmit={createProject}>
|
||||
<FormList>
|
||||
<Heading size="4" tag="h2">Create a new organization</Heading>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<FormList gap={16}>
|
||||
<InputText
|
||||
id="name"
|
||||
label="Project name"
|
||||
placeholder="First Appwrite Project"
|
||||
required
|
||||
label="Name"
|
||||
placeholder="Organization name"
|
||||
hideRequired
|
||||
bind:value={name} />
|
||||
{#if !showCustomId}
|
||||
<div>
|
||||
<Pill button on:click={() => (showCustomId = !showCustomId)}>
|
||||
<span class="icon-pencil" aria-hidden="true" /><span class="text">
|
||||
Project ID
|
||||
Organization ID
|
||||
</span>
|
||||
</Pill>
|
||||
</div>
|
||||
{:else}
|
||||
<CustomId bind:show={showCustomId} name="Project" isProject bind:id />
|
||||
<CustomId bind:show={showCustomId} name="Organization" isProject bind:id />
|
||||
{/if}
|
||||
<Button fullWidth submit disabled={name === ''} event="create_project">
|
||||
Create project
|
||||
{#if isCloud}
|
||||
<div class="u-margin-block-start-8">
|
||||
<h3><b>Plan</b></h3>
|
||||
<p>
|
||||
For more details on our plans, visit our <Button
|
||||
link
|
||||
external
|
||||
href="https://appwrite.io/pricing">pricing page</Button
|
||||
>.
|
||||
</p>
|
||||
<FormList class="u-margin-block-start-8">
|
||||
<InputSelect
|
||||
placeholder="Select a plan"
|
||||
label="Select plan"
|
||||
showLabel={false}
|
||||
id="plan"
|
||||
required
|
||||
hideRequired
|
||||
{options}
|
||||
bind:value={plan} />
|
||||
</FormList>
|
||||
</div>
|
||||
{/if}
|
||||
<Button fullWidth submit disabled={isCloud && !plan} event="create_organization">
|
||||
Get started
|
||||
</Button>
|
||||
</FormList>
|
||||
</Form>
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { Cover, CoverTitle } from '$lib/layout';
|
||||
import { Heading } from '$lib/components';
|
||||
import { Cover } from '$lib/layout';
|
||||
</script>
|
||||
|
||||
<Cover size="large">
|
||||
<CoverTitle>Let's create your first project</CoverTitle>
|
||||
<div class="u-flex u-cross-center u-main-center u-width-full-line">
|
||||
<Heading size="1" tag="h1" trimmed={false}>Welcome to Appwrite</Heading>
|
||||
</div>
|
||||
</Cover>
|
||||
Reference in new issue
Block a user