From ca24b0b82e5f372ed66115bd4aa2396f4caf12ca Mon Sep 17 00:00:00 2001 From: Arman Date: Thu, 28 Dec 2023 17:35:48 +0100 Subject: [PATCH] feat: new billing onboarding flow --- .../components/billing/paymentBoxes.svelte | 2 + src/lib/layout/wizard.svelte | 1 + src/lib/stores/stripe.ts | 10 +- src/lib/stores/wizard.ts | 8 +- .../console/createOrganizationCloud.svelte | 43 ++++- src/routes/console/onboarding/+page.svelte | 154 +++++++++++++----- src/routes/console/onboarding/header.svelte | 7 +- 7 files changed, 170 insertions(+), 55 deletions(-) diff --git a/src/lib/components/billing/paymentBoxes.svelte b/src/lib/components/billing/paymentBoxes.svelte index c6ead8911..240ad1c0c 100644 --- a/src/lib/components/billing/paymentBoxes.svelte +++ b/src/lib/components/billing/paymentBoxes.svelte @@ -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[]; export let group: string; @@ -33,6 +34,7 @@ onDestroy(() => { observer.disconnect(); + unmountPaymentElement(); }); $: if (element) { diff --git a/src/lib/layout/wizard.svelte b/src/lib/layout/wizard.svelte index 62f9652df..b55c4ff3c 100644 --- a/src/lib/layout/wizard.svelte +++ b/src/lib/layout/wizard.svelte @@ -122,6 +122,7 @@ } else { $wizard.step--; } + wizard.setInterceptor(null); trackEvent('wizard_back'); } diff --git a/src/lib/stores/stripe.ts b/src/lib/stores/stripe.ts index 98efcce63..e16a1354c 100644 --- a/src/lib/stores/stripe.ts +++ b/src/lib/stores/stripe.ts @@ -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 { diff --git a/src/lib/stores/wizard.ts b/src/lib/stores/wizard.ts index 85677ff0a..6a752584f 100644 --- a/src/lib/stores/wizard.ts +++ b/src/lib/stores/wizard.ts @@ -31,14 +31,18 @@ function createWizardStore() { return { subscribe, set, - start: (component: typeof SvelteComponent, media: string = null) => + start: ( + component: typeof SvelteComponent, + 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; diff --git a/src/routes/console/createOrganizationCloud.svelte b/src/routes/console/createOrganizationCloud.svelte index bfd188658..768231b24 100644 --- a/src/routes/console/createOrganizationCloud.svelte +++ b/src/routes/console/createOrganizationCloud.svelte @@ -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); diff --git a/src/routes/console/onboarding/+page.svelte b/src/routes/console/onboarding/+page.svelte index 0f84ec9dc..647399936 100644 --- a/src/routes/console/onboarding/+page.svelte +++ b/src/routes/console/onboarding/+page.svelte @@ -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); + } + } } -
- + Create a new organization + + {#if !showCustomId}
(showCustomId = !showCustomId)}>
{:else} - + {/if} - . +

+ + + + + {/if} +
diff --git a/src/routes/console/onboarding/header.svelte b/src/routes/console/onboarding/header.svelte index 51f7314c6..4bf595ce1 100644 --- a/src/routes/console/onboarding/header.svelte +++ b/src/routes/console/onboarding/header.svelte @@ -1,7 +1,10 @@ - Let's create your first project +
+ Welcome to Appwrite +