Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
470b047
feat: add Premium Geo DB addon to project settings
lohanidamodar Apr 18, 2026
5954b75
feat(settings): show price in premium geo DB enable dialog
lohanidamodar Apr 19, 2026
7e73d36
update sdk
lohanidamodar Apr 19, 2026
0e53210
feat(billing): render project addons in breakdown + use server addon …
lohanidamodar Apr 19, 2026
66e615e
Merge remote-tracking branch 'origin/main' into feat/project-premium-…
lohanidamodar Apr 20, 2026
afb13fc
Merge remote-tracking branch 'origin/main' into feat/project-premium-…
lohanidamodar Apr 21, 2026
13b0478
chore: bump @appwrite.io/console SDK to 95547fc
lohanidamodar Apr 23, 2026
0cba797
Merge remote-tracking branch 'origin/main' into feat/project-premium-…
lohanidamodar Apr 23, 2026
6fc000f
Merge remote-tracking branch 'origin/main' into feat/project-premium-…
lohanidamodar Apr 26, 2026
304142c
chore: keep @appwrite.io/console@95547fc for premium geo DB SDK methods
lohanidamodar Apr 26, 2026
67566db
Revert "chore: keep @appwrite.io/console@95547fc for premium geo DB S…
lohanidamodar Apr 26, 2026
e5a62c5
Merge remote-tracking branch 'origin/main' into feat/project-premium-…
lohanidamodar Apr 30, 2026
a54c530
chore: update console SDK to 352239b
lohanidamodar Apr 30, 2026
4376702
merge main
lohanidamodar May 4, 2026
da68f75
Merge remote-tracking branch 'origin/main' into feat/project-premium-…
lohanidamodar May 4, 2026
0889ace
feat(settings): rewrite Premium Geo DB card description to focus on d…
lohanidamodar May 6, 2026
6fbdd43
feat(settings): tighten Premium Geo DB card copy to outcome-focused v…
lohanidamodar May 7, 2026
1ba8ebd
Merge remote-tracking branch 'origin/main' into feat/project-premium-…
lohanidamodar May 11, 2026
f8d2816
chore: update console SDK to 634f110
lohanidamodar May 11, 2026
afecbd2
Merge remote-tracking branch 'origin/main' into feat/project-premium-…
lohanidamodar May 12, 2026
60838e0
fix(settings): handle 3DS redirect + replace cancel/retry with refres…
lohanidamodar May 12, 2026
279737d
Merge remote-tracking branch 'origin/main' into feat/project-premium-…
lohanidamodar May 13, 2026
51ad756
fix: rename Models.Locale to Models.CloudLocale after SDK bump
lohanidamodar May 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
},
"dependencies": {
"@ai-sdk/svelte": "^1.1.24",
"@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@f063676",
"@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@7df3842",
"@appwrite.io/pink-icons": "0.25.0",
"@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@bfe7ce3",
"@appwrite.io/pink-legacy": "^1.0.3",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,10 +249,6 @@
};

// addons (additional members, projects, etc.)
const billingAddonNames: Record<string, string> = {
addon_baa: 'HIPAA BAA'
};

const addons = (currentAggregation?.resources || [])
.filter(
(r) =>
Expand All @@ -269,8 +265,8 @@
? 'Additional members'
: addon.resourceId === 'projects'
? 'Additional projects'
: (billingAddonNames[addon.resourceId] ??
`${addon.resourceId} overage (${formatNum(addon.value)})`),
: addon.name ||
`${addon.resourceId} overage (${formatNum(addon.value)})`,
Comment thread
greptile-apps[bot] marked this conversation as resolved.
usage: '',
price: formatCurrency(addon.amount)
},
Expand Down Expand Up @@ -400,6 +396,18 @@
priceFormatter: ({ amount }) => formatCurrency(amount),
includeProgress: false
}),
...resources
.filter((r) => r.resourceId?.startsWith('addon_') && (r.amount ?? 0) > 0)
.map((addon) =>
createRow({
id: `addon-${addon.resourceId}`,
label: addon.name || addon.resourceId,
resource: addon,
usageFormatter: ({ value }) => formatNum(value),
priceFormatter: ({ amount }) => formatCurrency(amount),
includeProgress: false
})
),
Comment on lines +461 to +472
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Possible double-counting of addon charges in plan summary

The top-level addons section (line 252–275) already creates a billing row for every addon_-prefixed resource found in currentAggregation.resources. If the billing API also surfaces those same addon resources inside projectData.resources (the per-project breakdown), each project-scoped addon (e.g. Premium Geo DB) will appear as both a top-level line item and a child row — showing the same charge twice to the user.

Before shipping, confirm whether the cloud billing API places project-level addon charges exclusively in breakdown[].resources (making the top-level filter skip them) or in both places. If the former, this code is correct; if the latter, the top-level addons filter needs to exclude resources that are already accounted for at the project level (or vice-versa).

createRow({
id: 'usage-details',
label: `<a href="${base}/project-${String(projectData.region || 'default')}-${projectData.$id}/settings/usage" style="text-decoration: underline; color: var(--fgcolor-accent-neutral);">Usage details</a>`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
import UpdateVariables from '../updateVariables.svelte';
import { page } from '$app/state';
import UpdateLabels from './updateLabels.svelte';
import PremiumGeoDB from './premiumGeoDB.svelte';
import { isCloud } from '$lib/system';
import { ID } from '@appwrite.io/console';

export let data;
Expand Down Expand Up @@ -93,6 +95,9 @@
<UpdateProtocols />
<UpdateServices />
<UpdateInstallations {...data.installations} limit={data.limit} offset={data.offset} />
{#if isCloud}
<PremiumGeoDB addons={data.addons} addonPrice={data.addonPrice} />
{/if}
<UpdateVariables
{sdkCreateVariable}
{sdkUpdateVariable}
Expand Down
27 changes: 23 additions & 4 deletions src/routes/(console)/project-[region]-[project]/settings/+page.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,48 @@
import { Dependencies, PAGE_LIMIT } from '$lib/constants';
import { isCloud } from '$lib/system';
import { sdk } from '$lib/stores/sdk';
import { Query } from '@appwrite.io/console';
import { Addon, Query } from '@appwrite.io/console';
import type { PageLoad } from './$types';

export const load: PageLoad = async ({ depends, url, params }) => {
depends(Dependencies.PROJECT_VARIABLES);
depends(Dependencies.PROJECT_INSTALLATIONS);
depends(Dependencies.ADDONS);
const limit = PAGE_LIMIT;
const offset = Number(url.searchParams.get('offset') ?? 0);
const variablesOffset = Number(url.searchParams.get('variablesOffset') ?? 0);
const projectSdk = sdk.forProject(params.region, params.project);
const [variables, installations] = await Promise.all([
const [variables, installations, addons, addonPrice] = await Promise.all([
projectSdk.projectApi.listVariables({
queries: [Query.limit(limit), Query.offset(variablesOffset)]
}),
projectSdk.vcs.listInstallations({
queries: [Query.limit(limit), Query.offset(offset)]
})
}),
isCloud
? sdk
.forConsoleIn(params.region)
.projects.listAddons({ projectId: params.project })
.catch(() => null)
: Promise.resolve(null),
isCloud
? sdk
.forConsoleIn(params.region)
.projects.getAddonPrice({
projectId: params.project,
addon: Addon.Premiumgeodb
})
.catch(() => null)
: Promise.resolve(null)
]);

return {
limit,
offset,
variablesOffset,
variables,
installations
installations,
addons,
addonPrice
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
<script lang="ts">
import { Box, CardGrid } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { getChangePlanUrl, plansInfo } from '$lib/stores/billing';
import { currentPlan, organization } from '$lib/stores/organization';
import { page } from '$app/state';
import { get } from 'svelte/store';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { formatCurrency } from '$lib/helpers/numbers';
import { Badge } from '@appwrite.io/pink-svelte';
import type { Models } from '@appwrite.io/console';
import PremiumGeoDBEnableModal from './premiumGeoDBEnableModal.svelte';
import PremiumGeoDBDisableModal from './premiumGeoDBDisableModal.svelte';

export let addons: Models.AddonList | null = null;
export let addonPrice: Models.AddonPrice | null = null;

let showEnable = false;
let showDisable = false;
let reEnabling = false;
let cancelling = false;

$: planSupportsPremiumGeoDB = $currentPlan?.supportedAddons?.premiumGeoDB === true;
$: canUpgradeToPremiumGeoDB =
!planSupportsPremiumGeoDB && hasUpgradeablePlanWithPremiumGeoDB($currentPlan);
$: premiumGeoDBAddon = addons?.addons?.find(
(a) => a.key === 'premiumGeoDB' && (a.status === 'active' || a.status === 'pending')
);
$: isPending = premiumGeoDBAddon?.status === 'pending';
$: isActive = premiumGeoDBAddon?.status === 'active';
Comment on lines +33 to +35
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Addon key casing inconsistency with BAA pattern

The BAA.svelte filter uses the all-lowercase string 'baa', consistent with Appwrite's API key convention. Here 'premiumGeoDB' (camelCase) is used. If the API returns the addon with a lowercase key (following the same pattern as BAA), this find will never match — premiumGeoDBAddon would always be undefined and the component would always render the Enable CTA even when the addon is already active or pending. The same camelCase string is also used in the onMount fallback lookup at line 65, so both detection paths would silently fail. Please confirm the exact key string the API returns for this addon. What string does the API's listAddons response return for the key field of the Premium Geo DB addon — 'premiumGeoDB', 'premiumgeodb', or something else? BAA uses 'baa' (all lowercase), so camelCase here looks inconsistent with that established pattern.

$: isScheduledForRemoval = isActive && premiumGeoDBAddon?.nextValue === 0;
Comment thread
greptile-apps[bot] marked this conversation as resolved.
$: monthlyPriceLabel = addonPrice ? formatCurrency(addonPrice.monthlyPrice) : null;

function hasUpgradeablePlanWithPremiumGeoDB(plan: Models.BillingPlan): boolean {
if (!plan) return false;
const plans = get(plansInfo);
for (const [, p] of plans) {
if (p.order > plan.order && p.supportedAddons?.premiumGeoDB) {
return true;
}
}
return false;
}

async function handleCancelAndRetry() {
cancelling = true;
try {
await sdk.forConsoleIn(page.params.region).projects.deleteAddon({
projectId: page.params.project,
addonId: premiumGeoDBAddon.$id
});
await invalidate(Dependencies.ADDONS);
showEnable = true;
} catch (e) {
addNotification({
message: e.message,
type: 'error'
});
} finally {
cancelling = false;
}
}

async function handleReEnable() {
reEnabling = true;
try {
await sdk.forConsoleIn(page.params.region).projects.createPremiumGeoDBAddon({
projectId: page.params.project
});
await Promise.all([invalidate(Dependencies.ADDONS), invalidate(Dependencies.PROJECT)]);
addNotification({
message: 'Premium Geo DB addon has been re-enabled',
type: 'success'
});
} catch (e) {
addNotification({
message: e.message,
type: 'error'
});
} finally {
reEnabling = false;
}
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.
</script>
Comment thread
greptile-apps[bot] marked this conversation as resolved.

<CardGrid>
<svelte:fragment slot="title">Premium Geo DB</svelte:fragment>
Enrich session and request data with premium geolocation details such as timezone, postal code, ISP,
connection type, and organization. Useful for fine-grained analytics, fraud detection, and personalized
user experiences.
<svelte:fragment slot="aside">
<Box>
<h6>
<b>Premium Geo DB</b>
</h6>
{#if !planSupportsPremiumGeoDB && canUpgradeToPremiumGeoDB}
<p class="text u-margin-block-start-8">
Premium Geo DB is not available on your current plan. Upgrade your plan to
enable it.
</p>
<Button
secondary
class="u-margin-block-start-16"
href={getChangePlanUrl($organization?.$id)}>
<span class="text">Upgrade plan</span>
</Button>
{:else if !planSupportsPremiumGeoDB}
<p class="text u-margin-block-start-8">
Premium Geo DB is not available on your current plan.
</p>
{:else if isPending}
<div class="u-flex u-cross-center u-gap-8 u-margin-block-start-8">
<Badge variant="secondary" type="warning" content="Payment pending" />
</div>
<p class="text u-margin-block-start-8">
A payment is awaiting confirmation. If the payment was interrupted, you can
cancel and retry.
</p>
<Button
secondary
class="u-margin-block-start-16"
disabled={cancelling}
on:click={handleCancelAndRetry}>
<span class="text">Cancel & retry</span>
</Button>
Comment thread
greptile-apps[bot] marked this conversation as resolved.
{:else if isActive}
<div class="u-flex u-cross-center u-gap-8 u-margin-block-start-8">
{#if isScheduledForRemoval}
<Badge variant="secondary" type="warning" content="Scheduled for removal" />
{:else}
<Badge variant="secondary" type="success" content="Active" />
{/if}
</div>
<p class="text u-margin-block-start-8">
{#if monthlyPriceLabel}
Premium Geo DB is enabled for this project at {monthlyPriceLabel}/month.
{:else}
Premium Geo DB is enabled for this project.
{/if}
</p>
{#if isScheduledForRemoval}
<p class="text u-margin-block-start-8">
Premium Geo DB will be removed at the end of your current billing cycle.
</p>
<Button
secondary
class="u-margin-block-start-16"
disabled={reEnabling}
on:click={handleReEnable}>
<span class="text">Keep Premium Geo DB</span>
</Button>
{:else}
<Button
secondary
class="u-margin-block-start-16"
on:click={() => (showDisable = true)}>
<span class="text">Disable Premium Geo DB</span>
</Button>
{/if}
{:else}
<p class="text u-margin-block-start-8">
Enable Premium Geo DB for this project to collect detailed geolocation data on
every request.{#if monthlyPriceLabel}
This addon costs {monthlyPriceLabel}/month, prorated for your current
billing cycle.
{:else}
Billed prorated for your current cycle.
{/if}
</p>
<Button
secondary
class="u-margin-block-start-16"
on:click={() => (showEnable = true)}>
<span class="text">Enable Premium Geo DB</span>
</Button>
{/if}
</Box>
</svelte:fragment>
</CardGrid>

<PremiumGeoDBEnableModal bind:show={showEnable} {addonPrice} />

{#if premiumGeoDBAddon}
<PremiumGeoDBDisableModal bind:show={showDisable} addonId={premiumGeoDBAddon.$id} />
{/if}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<script lang="ts">
import { page } from '$app/state';
import { invalidate } from '$app/navigation';
import { Modal } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { Dependencies } from '$lib/constants';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';

let {
show = $bindable(false),
addonId
}: {
show: boolean;
addonId: string;
} = $props();

let error = $state<string | null>(null);
let submitting = $state(false);

async function handleSubmit() {
submitting = true;
error = null;
try {
await sdk.forConsoleIn(page.params.region).projects.deleteAddon({
projectId: page.params.project,
addonId
});
await Promise.all([invalidate(Dependencies.ADDONS), invalidate(Dependencies.PROJECT)]);
addNotification({
message:
'Premium Geo DB addon will be removed at the end of your current billing cycle',
type: 'success'
});
show = false;
} catch (e) {
error = e.message;
} finally {
submitting = false;
}
}
</script>

<Modal bind:error bind:show onSubmit={handleSubmit} title="Disable Premium Geo DB">
<p class="text">
Are you sure you want to disable the Premium Geo DB addon? The addon will remain active
until the end of your current billing cycle and will not be renewed.
</p>

<svelte:fragment slot="footer">
<Button text on:click={() => (show = false)}>Cancel</Button>
<Button secondary submit disabled={submitting}>Disable Premium Geo DB</Button>
</svelte:fragment>
</Modal>
Loading
Loading