Merge remote-tracking branch 'origin/main' into poc-invoice-cycle-ref

This commit is contained in:
Damodar Lohani
2025-01-26 06:47:47 +00:00
15 changed files with 200 additions and 182 deletions
+6 -6
View File
@@ -17,8 +17,8 @@ jobs:
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
@@ -54,8 +54,8 @@ jobs:
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
@@ -90,8 +90,8 @@ jobs:
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
+1 -1
View File
@@ -19,7 +19,7 @@
"e2e:ui": "playwright test tests/e2e --ui"
},
"dependencies": {
"@appwrite.io/console": "1.4.6",
"@appwrite.io/console": "1.4.7",
"@appwrite.io/pink": "0.25.0",
"@appwrite.io/pink-icons": "0.25.0",
"@popperjs/core": "^2.11.8",
+7 -7
View File
@@ -9,8 +9,8 @@ importers:
.:
dependencies:
'@appwrite.io/console':
specifier: 1.4.6
version: 1.4.6
specifier: 1.4.7
version: 1.4.7
'@appwrite.io/pink':
specifier: 0.25.0
version: 0.25.0
@@ -95,7 +95,7 @@ importers:
version: 6.6.3
'@testing-library/svelte':
specifier: ^5.2.4
version: 5.2.4(svelte@4.2.19)(vite@5.4.11(@types/node@22.9.0)(sass@1.81.0))(vitest@1.6.0)
version: 5.2.4(svelte@4.2.19)(vite@5.4.11(@types/node@22.9.0)(sass@1.81.0))(vitest@1.6.0(@types/node@22.9.0)(@vitest/ui@1.6.0)(jsdom@22.1.0)(sass@1.81.0))
'@testing-library/user-event':
specifier: ^14.5.2
version: 14.5.2(@testing-library/dom@10.4.0)
@@ -199,8 +199,8 @@ packages:
'@analytics/type-utils@0.6.2':
resolution: {integrity: sha512-TD+xbmsBLyYy/IxFimW/YL/9L2IEnM7/EoV9Aeh56U64Ify8o27HJcKjo38XY9Tcn0uOq1AX3thkKgvtWvwFQg==}
'@appwrite.io/console@1.4.6':
resolution: {integrity: sha512-oGA5gkecpy9E/fKJkaGn26JP9mgknc2Vn8hS6xhRBhmUPg8Vqlz0vgPpx1EQbPB2C5HWIf4yToSicOH31XGx6Q==}
'@appwrite.io/console@1.4.7':
resolution: {integrity: sha512-5zx+c5nWRm/UJNxgtOi4vj1pYW+wusfnOX2hqEGDLLuNZRc1rViYRefNuezosp5SPtjClCIYn9TGuFlz/XLwhw==}
'@appwrite.io/pink-icons@0.25.0':
resolution: {integrity: sha512-0O3i2oEuh5mWvjO80i+X6rbzrWLJ1m5wmv2/M3a1p2PyBJsFxN8xQMTEmTn3Wl/D26SsM7SpzbdW6gmfgoVU9Q==}
@@ -3834,7 +3834,7 @@ snapshots:
'@analytics/type-utils@0.6.2': {}
'@appwrite.io/console@1.4.6': {}
'@appwrite.io/console@1.4.7': {}
'@appwrite.io/pink-icons@0.25.0': {}
@@ -5075,7 +5075,7 @@ snapshots:
lodash: 4.17.21
redent: 3.0.0
'@testing-library/svelte@5.2.4(svelte@4.2.19)(vite@5.4.11(@types/node@22.9.0)(sass@1.81.0))(vitest@1.6.0)':
'@testing-library/svelte@5.2.4(svelte@4.2.19)(vite@5.4.11(@types/node@22.9.0)(sass@1.81.0))(vitest@1.6.0(@types/node@22.9.0)(@vitest/ui@1.6.0)(jsdom@22.1.0)(sass@1.81.0))':
dependencies:
'@testing-library/dom': 10.4.0
svelte: 4.2.19
@@ -1,7 +1,7 @@
<script lang="ts">
import { BillingPlan } from '$lib/constants';
import { formatNum } from '$lib/helpers/string';
import { plansInfo, tierFree, tierPro, type Tier } from '$lib/stores/billing';
import { plansInfo, tierFree, tierPro, tierScale, type Tier } from '$lib/stores/billing';
import { Card, SecondaryTabs, SecondaryTabsItem } from '..';
let selectedTab: Tier = BillingPlan.FREE;
@@ -23,11 +23,11 @@
on:click={() => (selectedTab = BillingPlan.PRO)}>
{tierPro.name}
</SecondaryTabsItem>
<!-- <SecondaryTabsItem
<SecondaryTabsItem
disabled={selectedTab === BillingPlan.SCALE}
on:click={() => (selectedTab = BillingPlan.SCALE)}>
{tierScale.name}
</SecondaryTabsItem> -->
</SecondaryTabsItem>
</SecondaryTabs>
</div>
@@ -109,7 +109,7 @@
<p class="u-margin-block-start-8">Everything in the Pro plan, plus:</p>
<ul class="un-order-list u-margin-inline-start-4">
<li>Unlimited seats</li>
<li>Organization roles <span class="inline-tag">Coming soon</span></li>
<li>Organization roles</li>
<li>SOC-2, HIPAA compliance</li>
<li>SSO <span class="inline-tag">Coming soon</span></li>
<li>Priority support</li>
+1 -1
View File
@@ -12,7 +12,7 @@
export function getFlag(country: string, width: number, height: number, quality: number) {
if (!isValueOfStringEnum(Flag, country)) return '';
let flag = sdk.forProject.avatars
let flag = sdk.forConsole.avatars
.getFlag(country, width * 2, height * 2, quality)
?.toString();
flag?.includes('&project=')
+13 -2
View File
@@ -4,10 +4,21 @@ import { base } from '$app/paths';
export function checkPricingRefAndRedirect(searchParams: URLSearchParams, shouldRegister = false) {
if (searchParams.has('type')) {
const paramType = searchParams.get('type');
const hasPlan = searchParams.has('plan');
if (paramType === 'create') {
shouldRegister
? goto(
`${base}/register?type=create${hasPlan ? `&plan=${searchParams.get('plan')}` : ''}`
)
: goto(
`${base}/create-organization?type=create${hasPlan ? `&plan=${searchParams.get('plan')}` : ''}`
);
}
//Legacy
if (paramType === 'createPro') {
shouldRegister
? goto(`${base}/register?type=createPro`)
: goto(`${base}/create-organization?type=createPro`);
? goto(`${base}/register?type=create&plan=tier-1`)
: goto(`${base}/create-organization?type=create&plan=tier-1`);
}
}
}
+1 -1
View File
@@ -129,7 +129,7 @@
<Pill button on:click={() => (showDropdown = !showDropdown)}>
<span class="icon-info" />{total}/{limit} created
</Pill>
{:else}
{:else if $organization?.billingPlan !== BillingPlan.SCALE}
<Pill button on:click={() => (showDropdown = !showDropdown)}>
<span class="icon-info" />Limits applied
</Pill>
+5 -3
View File
@@ -175,7 +175,7 @@ export type TierData = {
export const tierFree: TierData = {
name: 'Free',
description: 'For personal hobby projects of small scale and students.'
description: 'A great fit for passion projects and small applications.'
};
export const tierGitHubEducation: TierData = {
@@ -185,11 +185,13 @@ export const tierGitHubEducation: TierData = {
export const tierPro: TierData = {
name: 'Pro',
description: 'For pro developers and production projects that need the ability to scale.'
description:
'For production applications that need powerful functionality and resources to scale.'
};
export const tierScale: TierData = {
name: 'Scale',
description: 'For scaling teams and agencies that need dedicated support.'
description:
'For teams that handle more complex and large projects and need more control and support.'
};
export const tierCustom: TierData = {
+1 -2
View File
@@ -61,8 +61,7 @@ function createPreferences() {
getCustomCollectionColumns: (collectionId: string): Preferences['columns'] => {
return (
preferences[sdk.forProject.client.config.project]?.collections?.[collectionId] ??
null
preferences[sdk.forProject.client.config.project]?.collections?.[collectionId] ?? []
);
},
setLimit: (limit: Preferences['limit']) =>
+4 -4
View File
@@ -33,11 +33,11 @@
{
value: BillingPlan.PRO,
label: `${tierToPlan(BillingPlan.PRO).name} - ${formatCurrency($plansInfo.get(BillingPlan.PRO).price)} / month + add-ons`
},
{
value: BillingPlan.SCALE,
label: `${tierToPlan(BillingPlan.SCALE).name} - ${formatCurrency($plansInfo.get(BillingPlan.SCALE).price)}/month + usage`
}
// {
// value: BillingPlan.SCALE,
// label: `${tierToPlan(BillingPlan.SCALE).name} - ${formatCurrency($plansInfo.get(BillingPlan.SCALE).price)}/month + usage`
// }
]
: [];
@@ -29,10 +29,10 @@
import { trackEvent } from '$lib/actions/analytics';
import { selectedInvoice, showRetryModal } from './store';
import { organization } from '$lib/stores/organization';
import { BillingPlan } from '$lib/constants';
let showDropdown = [];
let showFailedError = false;
// let isLoadingInvoices = true;
let offset = 0;
let invoiceList: InvoiceList = {
@@ -46,11 +46,13 @@
onMount(request);
async function request() {
// isLoadingInvoices = true;
invoiceList = await sdk.forConsole.billing.listInvoices($page.params.organization, [
Query.limit(limit),
Query.offset(offset),
Query.orderDesc('$createdAt')
]);
// isLoadingInvoices = false;
}
function retryPayment(invoice: Invoice) {
@@ -62,144 +64,151 @@
request();
}
</script>
{#if $organization?.billingPlan !== BillingPlan.FREE && invoiceList.total > 0}
<CardGrid>
<Heading tag="h2" size="6">Payment history</Heading>
<p class="text">
Transaction history for this organization. Download invoices for more details about your
payments.
</p>
<svelte:fragment slot="aside">
{#if invoiceList.total > 0}
<TableScroll noMargin transparent noStyles>
<TableHeader>
<TableCellHead width={100}>Due Date</TableCellHead>
<TableCellHead width={80}>Status</TableCellHead>
<TableCellHead width={100}>Amount Due</TableCellHead>
<TableCellHead width={40} />
</TableHeader>
<TableBody>
{#each invoiceList?.invoices as invoice, i}
{@const status = invoice.status}
<TableRow>
<TableCellText title="date">
{toLocaleDate(invoice.dueAt)}
</TableCellText>
<TableCell title="status">
{#if invoice?.lastError}
<DropList bind:show={showFailedError}>
<Pill
danger={status === 'overdue' ||
status === 'failed' ||
status === 'requires_authentication'}
success={status === 'paid' ||
status === 'succeeded'}
warning={status === 'pending'}
on:click={() => (showFailedError = true)}
button>
{status === 'requires_authentication'
? 'failed'
: status}
</Pill>
<svelte:fragment slot="list">
<li>
The scheduled payment has failed.
<Button
link
on:click={() => {
retryPayment(invoice);
showFailedError = false;
}}
>Try again
</Button>
.
</li>
</svelte:fragment>
</DropList>
{:else}
<CardGrid>
<Heading tag="h2" size="6">Payment history</Heading>
<p class="text">
Transaction history for this organization. Download invoices for more details about your
payments.
</p>
<svelte:fragment slot="aside">
{#if invoiceList.total > 0}
<TableScroll noMargin transparent noStyles>
<TableHeader>
<TableCellHead width={100}>Due Date</TableCellHead>
<TableCellHead width={80}>Status</TableCellHead>
<TableCellHead width={100}>Amount Due</TableCellHead>
<TableCellHead width={40} />
</TableHeader>
<TableBody>
{#each invoiceList?.invoices as invoice, i}
{@const status = invoice.status}
<TableRow>
<TableCellText title="date">
{toLocaleDate(invoice.dueAt)}
</TableCellText>
<TableCell title="status">
{#if invoice?.lastError}
<DropList bind:show={showFailedError}>
<Pill
danger={status === 'overdue' ||
status === 'failed' ||
status === 'requires_authentication'}
success={status === 'paid' || status === 'succeeded'}
warning={status === 'pending'}>
warning={status === 'pending'}
on:click={() => (showFailedError = true)}
button>
{status === 'requires_authentication'
? 'failed'
: status}
</Pill>
{/if}
</TableCell>
<TableCellText title="due">
{formatCurrency(invoice.grossAmount)}
</TableCellText>
<TableCell showOverflow right>
<DropList
bind:show={showDropdown[i]}
placement="bottom-start"
noArrow>
<Button
round
text
noMargin
ariaLabel="More options"
on:click={() => {
showDropdown[i] = !showDropdown[i];
}}>
<span class="icon-dots-horizontal" aria-hidden="true" />
</Button>
<svelte:fragment slot="list">
<DropListLink
icon="external-link"
external
href={`${endpoint}/organizations/${$page.params.organization}/invoices/${invoice.$id}/view`}
on:click={() =>
(showDropdown[i] = !showDropdown[i])}
event="view_invoice">
View invoice
</DropListLink>
<DropListLink
icon="download"
href={`${endpoint}/organizations/${$page.params.organization}/invoices/${invoice.$id}/download`}
on:click={() => {
showDropdown[i] = !showDropdown[i];
}}
event="download_invoice">
Download PDF
</DropListLink>
{#if status === 'overdue' || status === 'failed'}
<DropListItem
icon="refresh"
<li>
The scheduled payment has failed.
<Button
link
on:click={() => {
retryPayment(invoice);
showDropdown[i] = !showDropdown[i];
trackEvent(`click_retry_payment`, {
from: 'button',
source: 'billing_invoice_menu'
});
}}>
Retry payment
</DropListItem>
{/if}
showFailedError = false;
}}
>Try again
</Button>
.
</li>
</svelte:fragment>
</DropList>
</TableCell>
</TableRow>
{/each}
</TableBody>
</TableScroll>
<div class="u-flex u-main-space-between">
<p class="text">Total results: {invoiceList?.total ?? 0}</p>
<PaginationInline {limit} bind:offset sum={invoiceList?.total ?? 0} hidePages />
</div>
{:else}
<EmptySearch hidePagination>
<p class="text u-text-center">
You have no payment history. After you receive your first invoice, you'll
see it here.
</p>
</EmptySearch>
{/if}
</svelte:fragment>
</CardGrid>
{/if}
{:else}
<Pill
danger={status === 'overdue' ||
status === 'failed' ||
status === 'requires_authentication'}
success={status === 'paid' || status === 'succeeded'}
warning={status === 'pending'}>
{status === 'requires_authentication' ? 'failed' : status}
</Pill>
{/if}
</TableCell>
<TableCellText title="due">
{formatCurrency(invoice.grossAmount)}
</TableCellText>
<TableCell showOverflow right>
<DropList
bind:show={showDropdown[i]}
placement="bottom-start"
noArrow>
<Button
round
text
noMargin
ariaLabel="More options"
on:click={() => {
showDropdown[i] = !showDropdown[i];
}}>
<span class="icon-dots-horizontal" aria-hidden="true" />
</Button>
<svelte:fragment slot="list">
<DropListLink
icon="external-link"
external
href={`${endpoint}/organizations/${$page.params.organization}/invoices/${invoice.$id}/view`}
on:click={() => (showDropdown[i] = !showDropdown[i])}
event="view_invoice">
View invoice
</DropListLink>
<DropListLink
icon="download"
href={`${endpoint}/organizations/${$page.params.organization}/invoices/${invoice.$id}/download`}
on:click={() => {
showDropdown[i] = !showDropdown[i];
}}
event="download_invoice">
Download PDF
</DropListLink>
{#if status === 'overdue' || status === 'failed'}
<DropListItem
icon="refresh"
on:click={() => {
retryPayment(invoice);
showDropdown[i] = !showDropdown[i];
trackEvent(`click_retry_payment`, {
from: 'button',
source: 'billing_invoice_menu'
});
}}>
Retry payment
</DropListItem>
{/if}
</svelte:fragment>
</DropList>
</TableCell>
</TableRow>
{/each}
</TableBody>
</TableScroll>
<div class="u-flex u-main-space-between">
<p class="text">Total results: {invoiceList.total}</p>
<PaginationInline {limit} bind:offset sum={invoiceList.total} hidePages />
</div>
<!--{:else if isLoadingInvoices}-->
<!-- <div class="loader-holder">-->
<!-- <div class="loader" />-->
<!-- </div>-->
{:else}
<EmptySearch hidePagination>
<p class="text u-text-center">
You have no payment history. After you receive your first invoice, you'll see it
here.
</p>
</EmptySearch>
{/if}
</svelte:fragment>
</CardGrid>
<!--<style>-->
<!-- .loader-holder {-->
<!-- height: 100%;-->
<!-- align-content: center;-->
<!-- justify-items: center;-->
<!-- }-->
<!--</style>-->
@@ -304,7 +304,7 @@
tier={BillingPlan.FREE}
class="u-margin-block-start-24"
members={data?.members?.total ?? 0} />
{:else if billingPlan === BillingPlan.PRO && $organization.billingPlan === BillingPlan.SCALE}
{:else if billingPlan === BillingPlan.PRO && $organization.billingPlan === BillingPlan.SCALE && collaborators?.length > 0}
{@const extraMembers = collaborators?.length ?? 0}
<Alert type="error" class="u-margin-block-start-24">
<svelte:fragment slot="title">
@@ -339,7 +339,7 @@
id="couponId" />
</FormList>
{/if}
{#if isDowngrade}
{#if isDowngrade && billingPlan === BillingPlan.FREE}
<FormList class="u-margin-block-start-24">
<InputSelect
id="reason"
@@ -142,7 +142,7 @@
<TableCellHead width={192}>Backups</TableCellHead>
<TableCellHead width={80}>Size</TableCellHead>
<TableCellHead width={120}>Status</TableCellHead>
<TableCellHead width={80}>Policy</TableCellHead>
<TableCellHead width={120}>Policy</TableCellHead>
<TableCellHead width={48} />
</TableHeader>
<TableBody>
@@ -92,7 +92,9 @@
);
return collections;
} else {
const collections = await sdk.forProject.databases.listCollections(databaseId);
const collections = await sdk.forProject.databases.listCollections(databaseId, [
Query.limit(100)
]);
return collections;
}
}
@@ -1,43 +1,37 @@
<script lang="ts">
import { Alert, CustomId } from '$lib/components';
import { BillingPlan } from '$lib/constants';
import { page } from '$app/stores';
import { Pill } from '$lib/elements';
import { Button, FormList, InputFile } from '$lib/elements/forms';
import { FormList, InputFile } from '$lib/elements/forms';
import { humanFileSize, sizeToBytes } from '$lib/helpers/sizeConvertion';
import WizardStep from '$lib/layout/wizardStep.svelte';
import { tierToPlan, upgradeURL } from '$lib/stores/billing';
import { organization, currentPlan } from '$lib/stores/organization';
import { currentPlan } from '$lib/stores/organization';
import { isCloud } from '$lib/system';
import { bucket } from '../store';
import { createFile } from './store';
import { base } from '$app/paths';
let showCustomId = false;
let fileError: string = null;
const service = $currentPlan['fileSize'];
const projectId = $page.params.project;
</script>
<WizardStep>
<svelte:fragment slot="title">File</svelte:fragment>
<svelte:fragment slot="subtitle">Upload a file to add it to your bucket.</svelte:fragment>
{#if isCloud}
{@const size = humanFileSize(sizeToBytes(service, 'MB', 1000))}
{@const plan = tierToPlan($organization?.billingPlan)}
{#if isCloud && fileError === 'File size exceeds the limit'}
{@const size = humanFileSize($bucket.maximumFileSize ?? sizeToBytes(service, 'MB', 1000))}
<Alert type="info">
<p class="text">
The {plan.name} plan has a maximum upload file size limit of {Math.floor(
parseInt(size.value)
)}{size.unit}.
{#if $organization?.billingPlan === BillingPlan.FREE}
Upgrade to allow files of a larger size.
{/if}
The maximum file upload size for this bucket is {parseInt(size.value)}{size.unit}.
You can adjust it in your
<a
href={`${base}/project-${projectId}/storage/bucket-${$bucket.$id}/settings`}
style:text-decoration="underline">bucket settings</a
>.
</p>
<svelte:fragment slot="action">
{#if $organization?.billingPlan === BillingPlan.FREE}
<div class="alert-buttons u-flex">
<Button text href={$upgradeURL}>Upgrade plan</Button>
</div>
{/if}
</svelte:fragment>
</Alert>
{/if}
@@ -47,6 +41,7 @@
required
bind:files={$createFile.files}
allowedFileExtensions={$bucket.allowedFileExtensions}
bind:error={fileError}
maxSize={isCloud
? $bucket.maximumFileSize
: ($bucket.maximumFileSize ?? sizeToBytes(service, 'MB', 1000))} />