From ca24b0b82e5f372ed66115bd4aa2396f4caf12ca Mon Sep 17 00:00:00 2001 From: Arman Date: Thu, 28 Dec 2023 17:35:48 +0100 Subject: [PATCH 1/7] 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 c6ead89112..240ad1c0cc 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 62f9652dfc..b55c4ff3c4 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 98efcce636..e16a1354c2 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 85677ff0a0..6a752584f5 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 bfd1886585..768231b24f 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 0f84ec9dc3..6473999367 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 isCloud} +
+

Plan

+

+ For more details on our plans, visit our . +

+ + + +
{/if} -
diff --git a/src/routes/console/onboarding/header.svelte b/src/routes/console/onboarding/header.svelte index 51f7314c6e..4bf595ce1c 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 +
From 6b305329002726a2e6a65623cde11550601dfe0f Mon Sep 17 00:00:00 2001 From: Arman Date: Thu, 28 Dec 2023 17:37:46 +0100 Subject: [PATCH 2/7] fix: org name --- src/routes/console/onboarding/+page.svelte | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/routes/console/onboarding/+page.svelte b/src/routes/console/onboarding/+page.svelte index 6473999367..fa7263dd4b 100644 --- a/src/routes/console/onboarding/+page.svelte +++ b/src/routes/console/onboarding/+page.svelte @@ -41,12 +41,13 @@ }); async function handleSubmit() { + const orgName = name?.length ? name : 'Personal Projects'; if (isCloud) { if (plan === BillingPlan.STARTER) { try { const org = await sdk.forConsole.billing.createOrganization( id ?? ID.unique(), - name ?? 'Personal Projects', + orgName, plan, null, null @@ -64,9 +65,7 @@ await invalidate(Dependencies.ACCOUNT); await goto(`/console/organization-${org.$id}`); addNotification({ - message: `${ - name?.length ? name : 'Personal Projects' - } organization successfully created`, + message: `${orgName} organization successfully created`, type: 'success' }); } catch (error) { @@ -78,16 +77,13 @@ } } else { wizard.start(CreateOrganizationCloud, null, 2); - $createOrganization.name = name?.length ? name : 'Personal Projects'; + $createOrganization.name = orgName; $createOrganization.billingPlan = plan; $createOrganization.id = id; } } else { try { - const org = await sdk.forConsole.teams.create( - id ?? ID.unique(), - name?.length ? name : 'Personal Projects' - ); + const org = await sdk.forConsole.teams.create(id ?? ID.unique(), orgName); const project = await sdk.forConsole.projects.create( ID.unique(), 'My first project', @@ -97,7 +93,7 @@ await invalidate(Dependencies.ACCOUNT); await goto(`/console/project-${project.$id}`); addNotification({ - message: `${name ?? 'Personal Projects'} organization successfully created`, + message: `${orgName} organization successfully created`, type: 'success' }); } catch (error) { From 99416057e3477f9e5fb3a8d4b55cd4188f36cee4 Mon Sep 17 00:00:00 2001 From: Arman Date: Thu, 28 Dec 2023 18:04:36 +0100 Subject: [PATCH 3/7] feat: add loading state to buttons while submitting --- src/lib/elements/forms/button.svelte | 9 ++++++++- src/routes/console/onboarding/+page.svelte | 14 ++++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/lib/elements/forms/button.svelte b/src/lib/elements/forms/button.svelte index 3b8647c217..30bcfb871c 100644 --- a/src/lib/elements/forms/button.svelte +++ b/src/lib/elements/forms/button.svelte @@ -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('form').isSubmitting @@ -77,6 +78,12 @@ aria-label={ariaLabel} type={submit ? 'submit' : 'button'} use:multiAction={actions}> - + {#if $isSubmitting && submissionLoader} +
{/if} - From 1257cdd455bc24754384f87b784bc0f246062555 Mon Sep 17 00:00:00 2001 From: Arman Date: Thu, 28 Dec 2023 18:28:30 +0100 Subject: [PATCH 4/7] feat: add confirmation modal on exit --- src/lib/layout/wizardExitModal.svelte | 9 ++++++++- src/routes/console/changeOrganizationTierCloud.svelte | 3 ++- src/routes/console/createOrganizationCloud.svelte | 3 ++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/lib/layout/wizardExitModal.svelte b/src/lib/layout/wizardExitModal.svelte index c3221c2683..c73d2cd2ed 100644 --- a/src/lib/layout/wizardExitModal.svelte +++ b/src/lib/layout/wizardExitModal.svelte @@ -8,10 +8,17 @@ function handleSubmit() { dispatch('exit'); + show = false; } - +

Are you sure you want to exit from ? All data will be deleted. This action is irreversible. diff --git a/src/routes/console/changeOrganizationTierCloud.svelte b/src/routes/console/changeOrganizationTierCloud.svelte index 309a8af8c1..cdad01a8e4 100644 --- a/src/routes/console/changeOrganizationTierCloud.svelte +++ b/src/routes/console/changeOrganizationTierCloud.svelte @@ -204,4 +204,5 @@ title="Change plan" steps={$changeTierSteps} finalAction={$changeOrganizationFinalAction} - on:exit={onFinish} /> + on:exit={onFinish} + confirmExit /> diff --git a/src/routes/console/createOrganizationCloud.svelte b/src/routes/console/createOrganizationCloud.svelte index 768231b24f..5ee87c9bad 100644 --- a/src/routes/console/createOrganizationCloud.svelte +++ b/src/routes/console/createOrganizationCloud.svelte @@ -160,4 +160,5 @@ title="Create organization" steps={$createOrgSteps} finalAction={$createOrganizationFinalAction} - on:exit={onFinish} /> + on:exit={onFinish} + confirmExit /> From 98b93a2264343d8c6a9eb7535196e2a420976631 Mon Sep 17 00:00:00 2001 From: Arman Date: Thu, 28 Dec 2023 18:38:38 +0100 Subject: [PATCH 5/7] fix: review --- src/lib/stores/stripe.ts | 4 +--- src/routes/console/createOrganizationCloud.svelte | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/lib/stores/stripe.ts b/src/lib/stores/stripe.ts index e16a1354c2..963adcf9e6 100644 --- a/src/lib/stores/stripe.ts +++ b/src/lib/stores/stripe.ts @@ -39,9 +39,7 @@ export async function initializeStripe() { export async function unmountPaymentElement() { isStripeInitialized.set(false); - if (paymentElement) { - paymentElement.unmount(); - } + paymentElement?.unmount(); clientSecret = null; paymentMethod = null; elements = null; diff --git a/src/routes/console/createOrganizationCloud.svelte b/src/routes/console/createOrganizationCloud.svelte index 5ee87c9bad..5e9b75e560 100644 --- a/src/routes/console/createOrganizationCloud.svelte +++ b/src/routes/console/createOrganizationCloud.svelte @@ -88,7 +88,7 @@ members_invited: $createOrganization?.collaborators?.length }); - //Create first pro project if onboarding + // Create first pro project if onboarding if ($page.url.pathname.includes('/console/onboarding')) { await sdk.forConsole.projects.create( ID.unique(), From 720f0ecd5e393105cfed8c67ecd666daddbe5315 Mon Sep 17 00:00:00 2001 From: Arman Date: Fri, 29 Dec 2023 10:00:35 +0100 Subject: [PATCH 6/7] feat: remove project creation --- .../console/createOrganizationCloud.svelte | 20 +------------------ src/routes/console/onboarding/+page.svelte | 20 +++++-------------- 2 files changed, 6 insertions(+), 34 deletions(-) diff --git a/src/routes/console/createOrganizationCloud.svelte b/src/routes/console/createOrganizationCloud.svelte index 5e9b75e560..894f9e518a 100644 --- a/src/routes/console/createOrganizationCloud.svelte +++ b/src/routes/console/createOrganizationCloud.svelte @@ -30,19 +30,13 @@ try { // Create free organization if coming from onboarding if ($page.url.pathname.includes('/console/onboarding')) { - const freeOrg = await sdk.forConsole.billing.createOrganization( + 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( @@ -88,18 +82,6 @@ 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}`); diff --git a/src/routes/console/onboarding/+page.svelte b/src/routes/console/onboarding/+page.svelte index 508314cc0a..8785fb04e8 100644 --- a/src/routes/console/onboarding/+page.svelte +++ b/src/routes/console/onboarding/+page.svelte @@ -25,8 +25,8 @@ let plan: Tier; const options = [ - { value: BillingPlan.STARTER, label: 'Starter - $0/billing period' }, - { value: BillingPlan.PRO, label: 'Pro - $15/billing period + add-ons' } + { value: BillingPlan.STARTER, label: 'Starter - $0/month' }, + { value: BillingPlan.PRO, label: 'Pro - $15/month + add-ons' } ]; onMount(() => { @@ -56,12 +56,6 @@ 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({ @@ -84,14 +78,10 @@ } else { try { const org = await sdk.forConsole.teams.create(id ?? ID.unique(), orgName); - 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}`); + await goto(`/console/organization-${org.$id}`); + addNotification({ message: `${orgName} organization successfully created`, type: 'success' From 203bd6dc4ec47b28524a2287225e1d5d4047ae4f Mon Sep 17 00:00:00 2001 From: Arman Date: Fri, 29 Dec 2023 10:38:12 +0100 Subject: [PATCH 7/7] fix: remove trial ending notification --- src/lib/stores/billing.ts | 21 ++------------------- src/routes/console/+layout.svelte | 2 -- 2 files changed, 2 insertions(+), 21 deletions(-) diff --git a/src/lib/stores/billing.ts b/src/lib/stores/billing.ts index 032dddf3b0..190db12348 100644 --- a/src/lib/stores/billing.ts +++ b/src/lib/stores/billing.ts @@ -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: `We hope you've been enjoying the ${ - tierToPlan(org.billingPlan).name - } plan. - You will be billed on a recurring 30-day cycle after your trial period ends on ${toLocaleDate( - org.billingStartDate - )}` - }); - 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>) { 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 diff --git a/src/routes/console/+layout.svelte b/src/routes/console/+layout.svelte index 49fcfde5fe..e6cdd038c1 100644 --- a/src/routes/console/+layout.svelte +++ b/src/routes/console/+layout.svelte @@ -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);