mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge pull request #1665 from appwrite/fix-appply-credits
Fix redirections after credits validation
This commit is contained in:
@@ -1,33 +1,28 @@
|
||||
<script lang="ts">
|
||||
import { afterNavigate, goto, invalidate } from '$app/navigation';
|
||||
import { afterNavigate, goto } from '$app/navigation';
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import {
|
||||
CreditsApplied,
|
||||
EstimatedTotalBox,
|
||||
SelectPaymentMethod
|
||||
} from '$lib/components/billing';
|
||||
import { BillingPlan, Dependencies } from '$lib/constants';
|
||||
import { Button, Form, FormList, InputSelect, InputTags, InputText } from '$lib/elements/forms';
|
||||
import { Button, Form, FormList, InputSelect } from '$lib/elements/forms';
|
||||
import { toLocaleDate } from '$lib/helpers/date';
|
||||
import {
|
||||
WizardSecondaryContainer,
|
||||
WizardSecondaryContent,
|
||||
WizardSecondaryFooter
|
||||
} from '$lib/layout';
|
||||
import { type PaymentList } from '$lib/sdk/billing';
|
||||
|
||||
import { app } from '$lib/stores/app';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { organizationList, type Organization } from '$lib/stores/organization';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { ID } from '@appwrite.io/console';
|
||||
import { onMount } from 'svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
import { CreditsApplied } from '$lib/components/billing';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { Helper } from '$lib/elements/forms/index.js';
|
||||
|
||||
export let data;
|
||||
|
||||
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,63}$/i;
|
||||
let previousPage: string = base;
|
||||
let showExitModal = false;
|
||||
let canSelectOrg = true;
|
||||
@@ -46,13 +41,7 @@
|
||||
|
||||
let selectedOrgId: string = null;
|
||||
let formComponent: Form;
|
||||
let couponForm: Form;
|
||||
let isSubmitting = writable(false);
|
||||
let methods: PaymentList;
|
||||
let paymentMethodId: string;
|
||||
let collaborators: string[];
|
||||
let taxId: string;
|
||||
let billingBudget: number;
|
||||
let newOrgId = ID.unique();
|
||||
let options = [
|
||||
...($organizationList?.teams?.map((team) => ({
|
||||
@@ -64,14 +53,12 @@
|
||||
label: 'Create new organization'
|
||||
}
|
||||
];
|
||||
let name: string;
|
||||
let coupon: string;
|
||||
|
||||
let couponData = data?.couponData;
|
||||
let campaign = data?.campaign;
|
||||
let billingPlan = BillingPlan.PRO;
|
||||
$: selectedAction = selectedOrgId === newOrgId ? 'create' : 'update';
|
||||
|
||||
onMount(async () => {
|
||||
await loadPaymentMethods();
|
||||
if (!$organizationList?.total || campaign?.onlyNewOrgs) {
|
||||
selectedOrgId = newOrgId;
|
||||
}
|
||||
@@ -79,109 +66,59 @@
|
||||
selectedOrgId = $page.url.searchParams.get('org');
|
||||
canSelectOrg = false;
|
||||
}
|
||||
if (campaign?.plan) {
|
||||
billingPlan = campaign.plan;
|
||||
}
|
||||
});
|
||||
|
||||
async function loadPaymentMethods() {
|
||||
const methodList = await sdk.forConsole.billing.listPaymentMethods();
|
||||
const filteredMethods = methodList.paymentMethods.filter((method) => !!method?.last4);
|
||||
methods = { paymentMethods: filteredMethods, total: filteredMethods.length };
|
||||
paymentMethodId =
|
||||
selectedOrg?.paymentMethodId ??
|
||||
methods.paymentMethods.find((method) => !!method?.last4)?.$id ??
|
||||
null;
|
||||
async function applyCouponCredit(org: Organization) {
|
||||
$isSubmitting = true;
|
||||
let error = null;
|
||||
|
||||
try {
|
||||
await sdk.forConsole.billing.addCredit(org.$id, couponData.code);
|
||||
trackEvent(Submit.CreditRedeem);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
$isSubmitting = false;
|
||||
trackError(error, Submit.CreditRedeem);
|
||||
}
|
||||
|
||||
addNotification({
|
||||
type: error ? 'error' : 'success',
|
||||
message: error ? error.message : 'Credit applied successfully'
|
||||
});
|
||||
|
||||
if (!error) {
|
||||
await goto(`${base}/organization-${selectedOrgId}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!couponForm.checkValidity()) return;
|
||||
try {
|
||||
let org: Organization;
|
||||
// Create new org
|
||||
if (selectedOrgId === newOrgId) {
|
||||
org = await sdk.forConsole.billing.createOrganization(
|
||||
newOrgId,
|
||||
name,
|
||||
billingPlan,
|
||||
paymentMethodId
|
||||
if (couponData) {
|
||||
const createOrganization = selectedAction === 'create';
|
||||
const isScalePlanUpgrade =
|
||||
campaign?.plan && selectedOrg?.billingPlan !== campaign?.plan;
|
||||
|
||||
// on create-org, its `coupon`
|
||||
if (createOrganization) {
|
||||
await goto(`${base}/create-organization?coupon=${couponData.code}`);
|
||||
return;
|
||||
}
|
||||
if (!campaign?.plan || campaign.plan === selectedOrg?.billingPlan) {
|
||||
await applyCouponCredit(selectedOrg);
|
||||
} else if (isScalePlanUpgrade) {
|
||||
await goto(
|
||||
`${base}/create-organization?coupon=${couponData.code}&plan=${campaign.plan}`
|
||||
);
|
||||
} else if (selectedAction === 'update') {
|
||||
// on change-plan, its `code`
|
||||
await goto(
|
||||
`${base}/organization-${selectedOrgId}/change-plan?code=${couponData.code}`
|
||||
);
|
||||
}
|
||||
// Upgrade existing org
|
||||
else if (selectedOrg?.billingPlan !== billingPlan) {
|
||||
org = await sdk.forConsole.billing.updatePlan(
|
||||
selectedOrg.$id,
|
||||
billingPlan,
|
||||
paymentMethodId,
|
||||
null
|
||||
);
|
||||
}
|
||||
// Existing pro org
|
||||
else {
|
||||
org = selectedOrg;
|
||||
}
|
||||
|
||||
// Add coupon
|
||||
if (couponData?.code) {
|
||||
await sdk.forConsole.billing.addCredit(org.$id, couponData.code);
|
||||
}
|
||||
|
||||
// Add budget
|
||||
if (billingBudget) {
|
||||
await sdk.forConsole.billing.updateBudget(org.$id, billingBudget, [75]);
|
||||
}
|
||||
|
||||
// Add collaborators
|
||||
if (collaborators?.length) {
|
||||
collaborators.forEach(async (collaborator) => {
|
||||
await sdk.forConsole.teams.createMembership(
|
||||
org.$id,
|
||||
['owner'],
|
||||
collaborator,
|
||||
undefined,
|
||||
undefined,
|
||||
`${$page.url.origin}${base}/invite`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Add tax ID
|
||||
if (taxId) {
|
||||
await sdk.forConsole.billing.updateTaxId(org.$id, taxId);
|
||||
}
|
||||
trackEvent(Submit.CreditRedeem, {
|
||||
coupon: couponData.code,
|
||||
campaign: couponData?.campaign
|
||||
});
|
||||
await invalidate(Dependencies.ORGANIZATION);
|
||||
await goto(`${base}/organization-${org.$id}`);
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'Credits applied successfully'
|
||||
});
|
||||
await invalidate(Dependencies.ACCOUNT);
|
||||
} catch (e) {
|
||||
trackError(e, Submit.CreditRedeem);
|
||||
} else {
|
||||
// we might not reach here but still, just being safe.
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: e.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function addCoupon() {
|
||||
try {
|
||||
const response = await sdk.forConsole.billing.getCoupon(coupon);
|
||||
couponData = response;
|
||||
coupon = null;
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'Credits applied successfully'
|
||||
});
|
||||
} catch (e) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: e.message
|
||||
message: 'Coupon code is not valid'
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -189,11 +126,6 @@
|
||||
$: selectedOrg = $organizationList?.teams?.find(
|
||||
(team) => team.$id === selectedOrgId
|
||||
) as Organization;
|
||||
|
||||
$: billingPlan =
|
||||
selectedOrg?.billingPlan === BillingPlan.SCALE
|
||||
? BillingPlan.SCALE
|
||||
: (campaign?.plan ?? BillingPlan.PRO);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -204,7 +136,7 @@
|
||||
<svelte:fragment slot="title">Apply credits</svelte:fragment>
|
||||
<WizardSecondaryContent>
|
||||
<Form bind:this={formComponent} onSubmit={handleSubmit} bind:isSubmitting>
|
||||
<FormList>
|
||||
<FormList gap={8}>
|
||||
{#if $organizationList?.total && !campaign?.onlyNewOrgs && canSelectOrg}
|
||||
<InputSelect
|
||||
bind:value={selectedOrgId}
|
||||
@@ -214,44 +146,16 @@
|
||||
placeholder="Select organization"
|
||||
id="organization" />
|
||||
{/if}
|
||||
{#if selectedOrgId && (selectedOrg?.billingPlan !== BillingPlan.PRO || !selectedOrg?.paymentMethodId)}
|
||||
{#if selectedOrgId === newOrgId}
|
||||
<InputText
|
||||
label="Organization name"
|
||||
placeholder="Enter organization name"
|
||||
id="name"
|
||||
required
|
||||
bind:value={name} />
|
||||
{/if}
|
||||
<InputTags
|
||||
bind:tags={collaborators}
|
||||
label="Invite members by email"
|
||||
tooltip="Invited members will have access to all services and payment data within your organization"
|
||||
placeholder="Enter email address(es)"
|
||||
validityRegex={emailRegex}
|
||||
validityMessage="Invalid email address"
|
||||
id="members" />
|
||||
<SelectPaymentMethod bind:methods bind:value={paymentMethodId} bind:taxId />
|
||||
{/if}
|
||||
</FormList>
|
||||
</Form>
|
||||
<Form bind:this={couponForm} onSubmit={addCoupon}>
|
||||
<FormList>
|
||||
{#if !data?.couponData?.code && selectedOrgId}
|
||||
<InputText
|
||||
required
|
||||
disabled={!!couponData?.credits}
|
||||
bind:value={coupon}
|
||||
placeholder="Enter coupon code"
|
||||
id="code"
|
||||
label="Coupon code">
|
||||
<Button submit secondary disabled={!!couponData?.credits}>
|
||||
<span class="text">Apply</span>
|
||||
</Button>
|
||||
</InputText>
|
||||
|
||||
{#if campaign?.plan && selectedOrg && selectedOrg.billingPlan !== campaign?.plan}
|
||||
<Helper type="neutral">
|
||||
This coupon code is only valid when upgrading to Scale plan.
|
||||
</Helper>
|
||||
{/if}
|
||||
</FormList>
|
||||
</Form>
|
||||
|
||||
<!-- this is the side card and is okay -->
|
||||
<svelte:fragment slot="aside">
|
||||
{#if campaign?.template === 'card'}
|
||||
<div
|
||||
@@ -273,44 +177,27 @@
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if selectedOrg?.$id && selectedOrg?.billingPlan !== BillingPlan.FREE && selectedOrg?.billingPlan !== BillingPlan.GITHUB_EDUCATION}
|
||||
<section
|
||||
class="card u-margin-block-start-24"
|
||||
style:--p-card-padding="1.5rem"
|
||||
style:--p-card-border-radius="var(--border-radius-small)">
|
||||
{#if couponData?.code && couponData?.status === 'active'}
|
||||
<CreditsApplied bind:couponData fixedCoupon={!!data?.couponData?.code} />
|
||||
<p class="text u-margin-block-start-12">
|
||||
|
||||
<section
|
||||
class="card u-margin-block-start-24"
|
||||
style:--p-card-padding="1.5rem"
|
||||
style:--p-card-border-radius="var(--border-radius-small)">
|
||||
{#if couponData?.code && couponData?.status === 'active'}
|
||||
{@const dateAvailable = selectedOrg?.billingNextInvoiceDate}
|
||||
<CreditsApplied bind:couponData fixedCoupon={!!data?.couponData?.code} />
|
||||
<p class="text u-margin-block-start-12">
|
||||
{#if !dateAvailable}
|
||||
Credits will automatically be applied to your next invoice.
|
||||
{:else}
|
||||
Credits will automatically be applied to your next invoice on <b
|
||||
>{toLocaleDate(selectedOrg?.billingNextInvoiceDate)}.</b>
|
||||
</p>
|
||||
{:else}
|
||||
<p class="text">Add a coupon code to apply credits to your organization.</p>
|
||||
{/if}
|
||||
</section>
|
||||
{:else if selectedOrgId}
|
||||
<div class:u-margin-block-start-24={campaign?.template === 'card'}>
|
||||
<EstimatedTotalBox
|
||||
fixedCoupon={!!data?.couponData?.code}
|
||||
{billingPlan}
|
||||
{collaborators}
|
||||
bind:couponData
|
||||
bind:billingBudget>
|
||||
{#if campaign?.template === 'review' && (campaign?.cta || campaign?.claimed || campaign?.unclaimed)}
|
||||
<div class="u-margin-block-end-24">
|
||||
<p class="body-text-1 u-bold">{campaign?.cta}</p>
|
||||
<p class="text u-margin-block-start-8">
|
||||
{#if couponData?.code && couponData?.status === 'active' && campaign?.claimed}
|
||||
{campaign?.claimed}
|
||||
{:else if campaign?.unclaimed}
|
||||
{campaign?.unclaimed}
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
>{toLocaleDate(selectedOrg?.billingNextInvoiceDate)}</b
|
||||
>.
|
||||
{/if}
|
||||
</EstimatedTotalBox>
|
||||
</div>
|
||||
{/if}
|
||||
</p>
|
||||
{:else}
|
||||
<p class="text">Add a coupon code to apply credits to your organization.</p>
|
||||
{/if}
|
||||
</section>
|
||||
</svelte:fragment>
|
||||
</WizardSecondaryContent>
|
||||
|
||||
@@ -319,18 +206,20 @@
|
||||
<Button
|
||||
fullWidthMobile
|
||||
on:click={() => {
|
||||
if (formComponent.checkValidity() && couponForm.checkValidity()) {
|
||||
if (formComponent.checkValidity()) {
|
||||
handleSubmit();
|
||||
}
|
||||
}}
|
||||
disabled={$isSubmitting}>
|
||||
disabled={$isSubmitting || !selectedOrgId}>
|
||||
{#if $isSubmitting}
|
||||
<span class="loader is-small is-transparent u-line-height-1-5" aria-hidden="true" />
|
||||
{/if}
|
||||
{#if selectedOrgId === newOrgId}
|
||||
Create Organization
|
||||
Create organization
|
||||
{:else if campaign?.plan && selectedOrg && selectedOrg.billingPlan !== campaign?.plan}
|
||||
Upgrade plan
|
||||
{:else}
|
||||
Apply Credits
|
||||
Apply credits
|
||||
{/if}
|
||||
</Button>
|
||||
</WizardSecondaryFooter>
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
let formComponent: Form;
|
||||
let isSubmitting = writable(false);
|
||||
let methods: PaymentList;
|
||||
let billingPlan: Tier = $organization.billingPlan;
|
||||
let billingPlan: Tier = $currentPlan?.$id as Tier;
|
||||
let paymentMethodId: string;
|
||||
let collaborators: string[] =
|
||||
data?.members?.memberships
|
||||
@@ -100,7 +100,7 @@
|
||||
billingPlan = plan as BillingPlan;
|
||||
}
|
||||
}
|
||||
if ($organization?.billingPlan === BillingPlan.SCALE) {
|
||||
if ($currentPlan?.$id === BillingPlan.SCALE) {
|
||||
billingPlan = BillingPlan.SCALE;
|
||||
} else {
|
||||
billingPlan = BillingPlan.PRO;
|
||||
@@ -141,7 +141,7 @@
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
from: tierToPlan($organization.billingPlan).name,
|
||||
from: tierToPlan($currentPlan?.$id as Tier).name,
|
||||
to: tierToPlan(billingPlan).name,
|
||||
email: $user.email,
|
||||
reason: feedbackDowngradeOptions.find(
|
||||
@@ -239,12 +239,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: isUpgrade = billingPlan > $organization.billingPlan;
|
||||
$: isDowngrade = billingPlan < $organization.billingPlan;
|
||||
$: isUpgrade = billingPlan > ($currentPlan?.$id as Tier);
|
||||
$: isDowngrade = billingPlan < ($currentPlan?.$id as Tier);
|
||||
$: if (billingPlan !== BillingPlan.FREE) {
|
||||
loadPaymentMethods();
|
||||
}
|
||||
$: isButtonDisabled = $organization.billingPlan === billingPlan;
|
||||
$: isButtonDisabled = ($currentPlan?.$id as Tier) === billingPlan;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -277,7 +277,7 @@
|
||||
tier={BillingPlan.FREE}
|
||||
class="u-margin-block-start-24"
|
||||
members={data?.members?.total ?? 0} />
|
||||
{:else if billingPlan === BillingPlan.PRO && $organization.billingPlan === BillingPlan.SCALE && collaborators?.length > 0}
|
||||
{:else if billingPlan === BillingPlan.PRO && $currentPlan?.$id === BillingPlan.SCALE && collaborators?.length > 0}
|
||||
{@const extraMembers = collaborators?.length ?? 0}
|
||||
<Alert type="error" class="u-margin-block-start-24">
|
||||
<svelte:fragment slot="title">
|
||||
@@ -294,7 +294,7 @@
|
||||
{/if}
|
||||
{/if}
|
||||
<!-- Show email input if upgrading from free plan -->
|
||||
{#if billingPlan !== BillingPlan.FREE && $organization.billingPlan === BillingPlan.FREE}
|
||||
{#if billingPlan !== BillingPlan.FREE && $currentPlan?.$id === BillingPlan.FREE}
|
||||
<FormList class="u-margin-block-start-16">
|
||||
<InputTags
|
||||
bind:tags={collaborators}
|
||||
@@ -335,14 +335,14 @@
|
||||
{/if}
|
||||
</Form>
|
||||
<svelte:fragment slot="aside">
|
||||
{#if billingPlan !== BillingPlan.FREE && $organization.billingPlan !== billingPlan && $organization.billingPlan !== BillingPlan.CUSTOM}
|
||||
{#if billingPlan !== BillingPlan.FREE && $currentPlan?.$id !== billingPlan && $currentPlan?.$id !== BillingPlan.CUSTOM}
|
||||
<EstimatedTotalBox
|
||||
{billingPlan}
|
||||
{collaborators}
|
||||
bind:couponData
|
||||
bind:billingBudget
|
||||
{isDowngrade} />
|
||||
{:else if $organization.billingPlan !== BillingPlan.CUSTOM}
|
||||
{:else if $currentPlan?.$id !== BillingPlan.CUSTOM}
|
||||
<PlanComparisonBox downgrade={isDowngrade} />
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
Reference in New Issue
Block a user