Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
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 on lines +268 to +269
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 BAA label regression if API omits name

The hardcoded billingAddonNames map (addon_baa → 'HIPAA BAA') has been removed in favour of addon.name. If the billing-aggregation API response doesn't populate name on existing BAA resources, BAA will display as addon_baa overage (1) in the plan summary for current BAA subscribers. Make sure the cloud API (and the new SDK pin) actually includes name on billing resource objects, or keep a fallback map alongside the API-provided name as a safety net.

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
})
),
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';
$: 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 on lines +67 to +86
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 handleReEnable silently drops 3DS response

createPremiumGeoDBAddon can return either a Models.Addon (immediate success) or a Models.PaymentAuthentication (3DS required). handleReEnable discards the return value entirely, so if re-enabling requires 3DS the payment challenge is never initiated — the notification fires as "re-enabled" while the addon actually sits in pending state waiting for a payment that will never complete.

The enable modal handles this correctly via the 'clientSecret' in result check and subsequent confirmPayment call. handleReEnable should mirror that same pattern, exactly as BAA.svelte's handleReEnable does.

</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>
{: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