mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge pull request #694 from appwrite/feat-new-billing-onboarding
feat: new billing onboarding flow
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
let classes: string = undefined;
|
||||
export { classes as class };
|
||||
export let actions: MultiActionArray = [];
|
||||
export let submissionLoader = false;
|
||||
|
||||
const isSubmitting = hasContext('form')
|
||||
? getContext<FormContext>('form').isSubmitting
|
||||
@@ -77,6 +78,12 @@
|
||||
aria-label={ariaLabel}
|
||||
type={submit ? 'submit' : 'button'}
|
||||
use:multiAction={actions}>
|
||||
<slot />
|
||||
{#if $isSubmitting && submissionLoader}
|
||||
<span
|
||||
class="loader is-small"
|
||||
style:--p-loader-base-full-color="transparent"
|
||||
aria-hidden="true" />
|
||||
{/if}
|
||||
<slot isSubmitting={$isSubmitting} />
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
@@ -122,6 +122,7 @@
|
||||
} else {
|
||||
$wizard.step--;
|
||||
}
|
||||
wizard.setInterceptor(null);
|
||||
trackEvent('wizard_back');
|
||||
}
|
||||
|
||||
|
||||
@@ -8,10 +8,17 @@
|
||||
|
||||
function handleSubmit() {
|
||||
dispatch('exit');
|
||||
show = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal title="Exit Process" bind:show onSubmit={handleSubmit} icon="exclamation" state="warning">
|
||||
<Modal
|
||||
title="Exit Process"
|
||||
bind:show
|
||||
onSubmit={handleSubmit}
|
||||
icon="exclamation"
|
||||
state="warning"
|
||||
headerDivider={false}>
|
||||
<p>
|
||||
Are you sure you want to exit from <slot />? All data will be deleted. This action is
|
||||
irreversible.
|
||||
|
||||
@@ -8,7 +8,7 @@ import { cachedStore } from '$lib/helpers/cache';
|
||||
import { Query, type Models } from '@appwrite.io/console';
|
||||
import { headerAlert } from './headerAlert';
|
||||
import PaymentAuthRequired from '$lib/components/billing/alerts/paymentAuthRequired.svelte';
|
||||
import { diffDays, toLocaleDate } from '$lib/helpers/date';
|
||||
import { diffDays } from '$lib/helpers/date';
|
||||
import { addNotification, notifications } from './notifications';
|
||||
import { goto } from '$app/navigation';
|
||||
import { base } from '$app/paths';
|
||||
@@ -166,24 +166,6 @@ export function calculateTrialDay(org: Organization) {
|
||||
return days;
|
||||
}
|
||||
|
||||
export function checkForTrialEnding(org: Organization) {
|
||||
const days = calculateTrialDay(org);
|
||||
if (localStorage.getItem('trialEndingNotification') === 'true' || !days) return;
|
||||
else if (days <= 5) {
|
||||
addNotification({
|
||||
type: 'info',
|
||||
isHtml: true,
|
||||
message: `<b>We hope you've been enjoying the ${
|
||||
tierToPlan(org.billingPlan).name
|
||||
} plan.</b>
|
||||
You will be billed on a recurring 30-day cycle after your trial period ends on <b>${toLocaleDate(
|
||||
org.billingStartDate
|
||||
)}</b>`
|
||||
});
|
||||
localStorage.setItem('trialEndingNotification', 'true');
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkForUsageLimit(org: Organization) {
|
||||
if (!org?.billingLimits) {
|
||||
readOnly.set(false);
|
||||
@@ -295,6 +277,7 @@ export async function checkForFreeOrgOverflow(orgs: Models.TeamList<Record<strin
|
||||
|
||||
export async function checkForPostReleaseProModal(orgs: Models.TeamList<Record<string, unknown>>) {
|
||||
if (!orgs?.teams?.length) return;
|
||||
if (orgs.total > orgs.teams.length) return; // if the total is greater that the free orgs it means that there are pro orgs
|
||||
const modalTime = localStorage.getItem('postReleaseProModal');
|
||||
const now = Date.now();
|
||||
// show the modal if it was never shown
|
||||
|
||||
@@ -37,7 +37,13 @@ export async function initializeStripe() {
|
||||
paymentElement.mount('#payment-element');
|
||||
}
|
||||
|
||||
// TODO: fix redirect
|
||||
export async function unmountPaymentElement() {
|
||||
isStripeInitialized.set(false);
|
||||
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;
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
checkForUsageLimit,
|
||||
checkPaymentAuthorizationRequired,
|
||||
calculateTrialDay,
|
||||
checkForTrialEnding,
|
||||
paymentExpired,
|
||||
checkForFreeOrgOverflow,
|
||||
checkForPostReleaseProModal,
|
||||
@@ -282,7 +281,6 @@
|
||||
if (!org) return;
|
||||
if (isCloud) {
|
||||
calculateTrialDay(org);
|
||||
checkForTrialEnding(org);
|
||||
await paymentExpired(org);
|
||||
await checkForUsageLimit(org);
|
||||
checkForMarkedForDeletion(org);
|
||||
|
||||
@@ -204,4 +204,5 @@
|
||||
title="Change plan"
|
||||
steps={$changeTierSteps}
|
||||
finalAction={$changeOrganizationFinalAction}
|
||||
on:exit={onFinish} />
|
||||
on:exit={onFinish}
|
||||
confirmExit />
|
||||
|
||||
@@ -28,6 +28,17 @@
|
||||
|
||||
async function create() {
|
||||
try {
|
||||
// Create free organization if coming from onboarding
|
||||
if ($page.url.pathname.includes('/console/onboarding')) {
|
||||
await sdk.forConsole.billing.createOrganization(
|
||||
ID.unique(),
|
||||
'Personal Projects',
|
||||
BillingPlan.STARTER,
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
const org = await sdk.forConsole.billing.createOrganization(
|
||||
$createOrganization.id ?? ID.unique(),
|
||||
$createOrganization.name,
|
||||
@@ -64,6 +75,13 @@
|
||||
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
|
||||
});
|
||||
|
||||
await invalidate(Dependencies.ACCOUNT);
|
||||
await preloadData(`/console/organization-${org.$id}`);
|
||||
await goto(`/console/organization-${org.$id}`);
|
||||
@@ -71,12 +89,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);
|
||||
@@ -129,4 +142,5 @@
|
||||
title="Create organization"
|
||||
steps={$createOrgSteps}
|
||||
finalAction={$createOrganizationFinalAction}
|
||||
on:exit={onFinish} />
|
||||
on:exit={onFinish}
|
||||
confirmExit />
|
||||
|
||||
@@ -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/month' },
|
||||
{ value: BillingPlan.PRO, label: 'Pro - $15/month + add-ons' }
|
||||
];
|
||||
|
||||
onMount(() => {
|
||||
if (isCloud) {
|
||||
@@ -32,66 +40,120 @@
|
||||
}
|
||||
});
|
||||
|
||||
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() {
|
||||
const orgName = name?.length ? name : 'Personal Projects';
|
||||
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(),
|
||||
orgName,
|
||||
plan,
|
||||
null,
|
||||
null
|
||||
);
|
||||
trackEvent(Submit.OrganizationCreate, {
|
||||
customId: !!id,
|
||||
plan: tierToPlan(plan)?.name
|
||||
});
|
||||
await invalidate(Dependencies.ACCOUNT);
|
||||
await goto(`/console/organization-${org.$id}`);
|
||||
addNotification({
|
||||
message: `${orgName} 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 = orgName;
|
||||
$createOrganization.billingPlan = plan;
|
||||
$createOrganization.id = id;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const org = await sdk.forConsole.teams.create(id ?? ID.unique(), orgName);
|
||||
|
||||
await invalidate(Dependencies.ACCOUNT);
|
||||
await goto(`/console/organization-${org.$id}`);
|
||||
|
||||
addNotification({
|
||||
message: `${orgName} 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"
|
||||
submissionLoader
|
||||
let:isSubmitting>
|
||||
{#if isSubmitting}
|
||||
Creating your first organization
|
||||
{:else}
|
||||
Get started
|
||||
{/if}
|
||||
</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