diff --git a/bun.lock b/bun.lock index 12377f9149..cfcf28654e 100644 --- a/bun.lock +++ b/bun.lock @@ -6,7 +6,7 @@ "name": "@appwrite/console", "dependencies": { "@ai-sdk/svelte": "^1.1.24", - "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@8e7decc", + "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@86906d2", "@appwrite.io/pink-icons": "0.25.0", "@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@df765cc", "@appwrite.io/pink-legacy": "^1.0.3", @@ -108,7 +108,7 @@ "@analytics/type-utils": ["@analytics/type-utils@0.6.4", "", {}, "sha512-Ou1gQxFakOWLcPnbFVsrPb8g1wLLUZYYJXDPjHkG07+5mustGs5yqACx42UAu4A6NszNN6Z5gGxhyH45zPWRxw=="], - "@appwrite.io/console": ["@appwrite.io/console@https://pkg.vc/-/@appwrite/@appwrite.io/console@8e7decc", { "dependencies": { "json-bigint": "1.0.0" } }], + "@appwrite.io/console": ["@appwrite.io/console@https://pkg.vc/-/@appwrite/@appwrite.io/console@86906d2", { "dependencies": { "json-bigint": "1.0.0" } }], "@appwrite.io/pink-icons": ["@appwrite.io/pink-icons@0.25.0", "", {}, "sha512-0O3i2oEuh5mWvjO80i+X6rbzrWLJ1m5wmv2/M3a1p2PyBJsFxN8xQMTEmTn3Wl/D26SsM7SpzbdW6gmfgoVU9Q=="], diff --git a/package.json b/package.json index 0ff0eb93c9..e7a228a5b8 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ }, "dependencies": { "@ai-sdk/svelte": "^1.1.24", - "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@8e7decc", + "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@86906d2", "@appwrite.io/pink-icons": "0.25.0", "@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@df765cc", "@appwrite.io/pink-legacy": "^1.0.3", diff --git a/src/lib/actions/analytics.ts b/src/lib/actions/analytics.ts index 023e2fbeee..396774c297 100644 --- a/src/lib/actions/analytics.ts +++ b/src/lib/actions/analytics.ts @@ -426,6 +426,8 @@ export enum Submit { MessagingTopicSubscriberDelete = 'submit_messaging_topic_subscriber_delete', ApplyQuickFilter = 'submit_apply_quick_filter', RequestBAA = 'submit_request_baa', + BAAAddonEnable = 'submit_baa_addon_enable', + BAAAddonDisable = 'submit_baa_addon_disable', RequestSoc2 = 'submit_request_soc2', SiteCreate = 'submit_site_create', SiteDelete = 'submit_site_delete', diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 2cb52605e3..549c56fd61 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -85,7 +85,8 @@ export enum Dependencies { MESSAGING_TOPIC_SUBSCRIBERS = 'dependency:messaging_topic_subscribers', SITE = 'dependency:site', SITES = 'dependency:sites', - SITES_DOMAINS = 'dependency:sites_domains' + SITES_DOMAINS = 'dependency:sites_domains', + ADDONS = 'dependency:addons' } export const defaultScopes: string[] = [ diff --git a/src/lib/stores/migration.ts b/src/lib/stores/migration.ts index 534cddc6a9..2dcb58df76 100644 --- a/src/lib/stores/migration.ts +++ b/src/lib/stores/migration.ts @@ -1,5 +1,5 @@ import { writable } from 'svelte/store'; -import { Resources } from '@appwrite.io/console'; +import { AppwriteMigrationResource as Resources } from '@appwrite.io/console'; import { includesAll } from '$lib/helpers/array'; const initialFormData = { diff --git a/src/routes/(console)/(migration-wizard)/resource-form.svelte b/src/routes/(console)/(migration-wizard)/resource-form.svelte index 54e3f23ba4..39ebbd1d51 100644 --- a/src/routes/(console)/(migration-wizard)/resource-form.svelte +++ b/src/routes/(console)/(migration-wizard)/resource-form.svelte @@ -11,7 +11,7 @@ } from '$lib/stores/migration'; import { Button } from '$lib/elements/forms'; import { wizard } from '$lib/stores/wizard'; - import { Resources, type Models } from '@appwrite.io/console'; + import { AppwriteMigrationResource as Resources, type Models } from '@appwrite.io/console'; import type { sdk } from '$lib/stores/sdk'; import ImportReport from '$routes/(console)/project-[region]-[project]/settings/migrations/(import)/importReport.svelte'; diff --git a/src/routes/(console)/organization-[organization]/billing/+page.ts b/src/routes/(console)/organization-[organization]/billing/+page.ts index 4c10658caa..202a6ee25c 100644 --- a/src/routes/(console)/organization-[organization]/billing/+page.ts +++ b/src/routes/(console)/organization-[organization]/billing/+page.ts @@ -25,6 +25,7 @@ export const load: PageLoad = async ({ parent, depends, url, route }) => { depends(Dependencies.CREDIT); depends(Dependencies.INVOICES); depends(Dependencies.ADDRESS); + depends(Dependencies.ADDONS); // aggregation reloads on page param changes depends(Dependencies.BILLING_AGGREGATION); diff --git a/src/routes/(console)/organization-[organization]/billing/planSummary.svelte b/src/routes/(console)/organization-[organization]/billing/planSummary.svelte index afe13b5172..1acbebfdb7 100644 --- a/src/routes/(console)/organization-[organization]/billing/planSummary.svelte +++ b/src/routes/(console)/organization-[organization]/billing/planSummary.svelte @@ -247,8 +247,17 @@ }; // addons (additional members, projects, etc.) + const billingAddonNames: Record = { + addon_baa: 'HIPAA BAA' + }; + const addons = (currentAggregation?.resources || []) - .filter((r) => r.amount > 0 && currentPlan?.addons?.[r.resourceId]?.price > 0) + .filter( + (r) => + r.amount > 0 && + (currentPlan?.addons?.[r.resourceId]?.price > 0 || + r.resourceId.startsWith('addon_')) + ) .map((addon) => ({ id: `addon-${addon.resourceId}`, expandable: false, @@ -258,7 +267,8 @@ ? 'Additional members' : addon.resourceId === 'projects' ? 'Additional projects' - : `${addon.resourceId} overage (${formatNum(addon.value)})`, + : (billingAddonNames[addon.resourceId] ?? + `${addon.resourceId} overage (${formatNum(addon.value)})`), usage: '', price: formatCurrency(addon.amount) }, diff --git a/src/routes/(console)/organization-[organization]/settings/+page.svelte b/src/routes/(console)/organization-[organization]/settings/+page.svelte index b5bf05fc0d..a303b98f5e 100644 --- a/src/routes/(console)/organization-[organization]/settings/+page.svelte +++ b/src/routes/(console)/organization-[organization]/settings/+page.svelte @@ -6,9 +6,11 @@ import { sdk } from '$lib/stores/sdk'; import { members, organization } from '$lib/stores/organization'; import { projects } from '../store'; - import { invalidate } from '$app/navigation'; + import { goto, invalidate } from '$app/navigation'; + import { resolve } from '$app/paths'; import { Dependencies } from '$lib/constants'; import { onMount } from 'svelte'; + import { page } from '$app/state'; import Delete from './deleteOrganizationModal.svelte'; import DownloadDPA from './downloadDPA.svelte'; import { Submit, trackEvent, trackError } from '$lib/actions/analytics'; @@ -22,8 +24,66 @@ let name: string; let showDelete = false; - onMount(() => { + onMount(async () => { name = $organization.name; + + if (page.url.searchParams.get('type') === 'validate-addon') { + let addonId = page.url.searchParams.get('addonId'); + + // Fall back to listing addons if addonId is missing or invalid + if (!addonId || addonId === 'undefined') { + try { + const addons = await sdk.forConsole.organizations.listAddons({ + organizationId: $organization.$id + }); + const pending = addons.addons.find( + (a) => a.key === 'baa' && a.status === 'pending' + ); + addonId = pending?.$id ?? null; + } catch { + addonId = null; + } + } + + if (addonId) { + try { + await sdk.forConsole.organizations.validateAddonPayment({ + organizationId: $organization.$id, + addonId + }); + await Promise.all([ + invalidate(Dependencies.ADDONS), + invalidate(Dependencies.ORGANIZATION) + ]); + addNotification({ + message: 'BAA addon has been enabled', + type: 'success' + }); + } catch (e) { + // If addon not found, payment webhook may have already activated it + if (e?.type === 'addon_not_found' || e?.code === 404) { + await Promise.all([ + invalidate(Dependencies.ADDONS), + invalidate(Dependencies.ORGANIZATION) + ]); + addNotification({ + message: 'BAA addon has been enabled', + type: 'success' + }); + } else { + addNotification({ + message: e.message, + type: 'error' + }); + } + } + } + + const settingsUrl = resolve('/(console)/organization-[organization]/settings', { + organization: $organization.$id + }); + await goto(settingsUrl, { replaceState: true }); + } }); async function updateName() { @@ -75,7 +135,7 @@ {#if isCloud} - + {/if} diff --git a/src/routes/(console)/organization-[organization]/settings/+page.ts b/src/routes/(console)/organization-[organization]/settings/+page.ts index 1f377bfe33..8ec4edb17b 100644 --- a/src/routes/(console)/organization-[organization]/settings/+page.ts +++ b/src/routes/(console)/organization-[organization]/settings/+page.ts @@ -7,8 +7,9 @@ import { isCloud } from '$lib/system'; export const load: PageLoad = async ({ depends, params, parent }) => { const { countryList, locale } = await parent(); depends(Dependencies.ORGANIZATION); + depends(Dependencies.ADDONS); - const [projects, invoices] = await Promise.all([ + const [projects, invoices, addons] = await Promise.all([ sdk.forConsole.projects.list({ queries: [Query.equal('teamId', params.organization), Query.select(['$id', 'name'])] }), @@ -16,12 +17,20 @@ export const load: PageLoad = async ({ depends, params, parent }) => { ? sdk.forConsole.organizations.listInvoices({ organizationId: params.organization }) - : undefined + : undefined, + isCloud + ? sdk.forConsole.organizations + .listAddons({ + organizationId: params.organization + }) + .catch(() => null) + : null ]); return { projects, invoices, + addons, countryList, locale }; diff --git a/src/routes/(console)/organization-[organization]/settings/BAA.svelte b/src/routes/(console)/organization-[organization]/settings/BAA.svelte index 87b33da1f1..5a458da47a 100644 --- a/src/routes/(console)/organization-[organization]/settings/BAA.svelte +++ b/src/routes/(console)/organization-[organization]/settings/BAA.svelte @@ -1,38 +1,181 @@ BAA - After requesting a BAA, we will contact you via email for the next steps. + A Business Associate Agreement (BAA) is a HIPAA-required document ensuring outside services handling + patient information for a healthcare organization follow privacy rules.
Business Associate Agreement (BAA)
-

- A Business Associate Agreement (BAA) is a HIPAA-required document ensuring outside - services handling patient information for a healthcare organization follow privacy - rules. -

- + {#if !planSupportsBaa && canUpgradeToBaa} +

+ BAA is not available on your current plan. Upgrade your plan to enable BAA for + your organization. +

+ + {:else if !planSupportsBaa} +

+ BAA is not available on your current plan. +

+ {:else if isPending} +
+ +
+

+ A payment is awaiting confirmation. If the payment was interrupted, you can + cancel and retry. +

+ + {:else if isActive} +
+ {#if isScheduledForRemoval} + + {:else} + + {/if} +
+

+ BAA is enabled for your organization at $350/month. +

+ {#if isScheduledForRemoval} +

+ BAA will be removed at the end of your current billing cycle. +

+ + {:else} + + {/if} + {:else} +

+ Enable BAA for your organization to ensure HIPAA compliance. This addon costs + $350/month, prorated for your current billing cycle. +

+ + {/if}
- + + +{#if baaAddon} + +{/if} diff --git a/src/routes/(console)/organization-[organization]/settings/BAADisableModal.svelte b/src/routes/(console)/organization-[organization]/settings/BAADisableModal.svelte new file mode 100644 index 0000000000..a1a5817bce --- /dev/null +++ b/src/routes/(console)/organization-[organization]/settings/BAADisableModal.svelte @@ -0,0 +1,54 @@ + + + +

+ Are you sure you want to disable the BAA addon? The addon will remain active until the end + of your current billing cycle and will not be renewed. +

+ + + + + +
diff --git a/src/routes/(console)/organization-[organization]/settings/BAAEnableModal.svelte b/src/routes/(console)/organization-[organization]/settings/BAAEnableModal.svelte new file mode 100644 index 0000000000..6aa4fe7fee --- /dev/null +++ b/src/routes/(console)/organization-[organization]/settings/BAAEnableModal.svelte @@ -0,0 +1,159 @@ + + + +

+ By clicking Accept & Enable, the amount of + {formatCurrency(BAA_MONTHLY_PRICE)} will be added to your subscription and your + payment method will be charged + {formatCurrency(proratedAmount)} immediately for the remaining days in your billing cycle. +

+

+ Your action confirms acceptance of Appwrite's + Business Associate Agreement + and related terms. +

+ +
+
+ HIPAA BAA + {formatCurrency(BAA_MONTHLY_PRICE)} / month +
+
+
+ Total + {formatCurrency(BAA_MONTHLY_PRICE)} / month +
+

+ * Plus applicable tax and fees +

+
+ + + + + +
+ + diff --git a/src/routes/(console)/organization-[organization]/settings/BAAModal.svelte b/src/routes/(console)/organization-[organization]/settings/BAAModal.svelte deleted file mode 100644 index 1aaccfef2a..0000000000 --- a/src/routes/(console)/organization-[organization]/settings/BAAModal.svelte +++ /dev/null @@ -1,125 +0,0 @@ - - - - - - - - - - - - -