Merge pull request #1665 from appwrite/fix-appply-credits

Fix redirections after credits validation
This commit is contained in:
Damodar Lohani
2025-02-14 12:19:11 +05:45
committed by GitHub
2 changed files with 96 additions and 207 deletions
+86 -197
View File
@@ -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>