diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index ab33a8f37..91c98b81f 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -42,6 +42,7 @@ jobs: "PUBLIC_GROWTH_ENDPOINT=${{ secrets.PUBLIC_GROWTH_ENDPOINT }}" "PUBLIC_STRIPE_KEY=${{ secrets.PUBLIC_STRIPE_KEY }}" "SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}" + "SENTRY_RELEASE=${{ github.event.release.tag_name }}" publish-cloud-stage: runs-on: ubuntu-latest steps: diff --git a/Dockerfile b/Dockerfile index 2730dccb9..45263f235 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,6 +6,7 @@ ENV PNPM_HOME="/pnpm" ENV PATH="$PNPM_HOME:$PATH" RUN npm i -g corepack@latest RUN corepack enable +RUN corepack prepare pnpm@10.0.0 --activate ADD ./package.json /app/package.json ADD ./pnpm-lock.yaml /app/pnpm-lock.yaml @@ -24,12 +25,14 @@ ARG PUBLIC_APPWRITE_ENDPOINT ARG PUBLIC_GROWTH_ENDPOINT ARG PUBLIC_STRIPE_KEY ARG SENTRY_AUTH_TOKEN +ARG SENTRY_RELEASE ENV PUBLIC_APPWRITE_ENDPOINT=$PUBLIC_APPWRITE_ENDPOINT ENV PUBLIC_GROWTH_ENDPOINT=$PUBLIC_GROWTH_ENDPOINT ENV PUBLIC_CONSOLE_MODE=$PUBLIC_CONSOLE_MODE ENV PUBLIC_STRIPE_KEY=$PUBLIC_STRIPE_KEY ENV SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN +ENV SENTRY_RELEASE=$SENTRY_RELEASE ENV NODE_OPTIONS=--max_old_space_size=8192 RUN pnpm run build @@ -39,4 +42,4 @@ FROM nginx:1.25-alpine EXPOSE 80 COPY docker/nginx.conf /etc/nginx/conf.d/default.conf -COPY --from=build /app/build /usr/share/nginx/html/console \ No newline at end of file +COPY --from=build /app/build /usr/share/nginx/html/console diff --git a/package.json b/package.json index 461a4249c..f0ef3aee1 100644 --- a/package.json +++ b/package.json @@ -21,11 +21,13 @@ "e2e:ui": "playwright test --ui" }, "dependencies": { - "@appwrite.io/console": "https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@6dbeff0", + "@ai-sdk/svelte": "^1.1.22", + "@appwrite.io/console": "https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@f76aff1", + "@appwrite.io/pink": "0.25.0", "@appwrite.io/pink-icons": "0.25.0", - "@appwrite.io/pink-icons-svelte": "https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@c339db5", + "@appwrite.io/pink-icons-svelte": "https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@bd21ff7f", "@appwrite.io/pink-legacy": "^1.0.3", - "@appwrite.io/pink-svelte": "https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@c339db5", + "@appwrite.io/pink-svelte": "https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@504c653", "@popperjs/core": "^2.11.8", "@sentry/sveltekit": "^8.38.0", "@stripe/stripe-js": "^3.5.0", diff --git a/playwright.config.ts b/playwright.config.ts index bad8f9fca..de119fa1f 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -13,7 +13,7 @@ const config: PlaywrightTestConfig = { webServer: { timeout: 120000, env: { - PUBLIC_APPWRITE_ENDPOINT: 'https://console-testing-2.appwrite.org/v1', + PUBLIC_APPWRITE_ENDPOINT: 'https://stage.cloud.appwrite.io/v1', PUBLIC_CONSOLE_MODE: 'cloud', PUBLIC_STRIPE_KEY: 'pk_test_51LT5nsGYD1ySxNCyd7b304wPD8Y1XKKWR6hqo6cu3GIRwgvcVNzoZv4vKt5DfYXL1gRGw4JOqE19afwkJYJq1g3K004eVfpdWn' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fb6093c59..78e5ecd08 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,21 +8,27 @@ importers: .: dependencies: + '@ai-sdk/svelte': + specifier: ^1.1.22 + version: 1.1.22(svelte@5.25.3)(zod@3.24.3) '@appwrite.io/console': - specifier: https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@6dbeff0 - version: https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@6dbeff0 + specifier: https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@f76aff1 + version: https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@f76aff1 + '@appwrite.io/pink': + specifier: 0.25.0 + version: 0.25.0 '@appwrite.io/pink-icons': specifier: 0.25.0 version: 0.25.0 '@appwrite.io/pink-icons-svelte': - specifier: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@c339db5 - version: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@c339db5(svelte@5.25.3) + specifier: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@bd21ff7f + version: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@bd21ff7f(svelte@5.25.3) '@appwrite.io/pink-legacy': specifier: ^1.0.3 version: 1.0.3 '@appwrite.io/pink-svelte': - specifier: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@c339db5 - version: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@c339db5(react-dom@18.3.1(react@18.3.1))(svelte@5.25.3) + specifier: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@504c653 + version: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@504c653(react-dom@18.3.1(react@18.3.1))(svelte@5.25.3) '@popperjs/core': specifier: ^2.11.8 version: 2.11.8 @@ -192,6 +198,37 @@ packages: '@adobe/css-tools@4.4.2': resolution: {integrity: sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A==} + '@ai-sdk/provider-utils@2.1.11': + resolution: {integrity: sha512-lMnXA5KaRJidzW7gQmlo/SnX6D+AKk5GxHFcQtOaGOSJNmu/qcNZc1rGaO7K5qW52OvCLXtnWudR4cc/FvMpVQ==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.0.0 + peerDependenciesMeta: + zod: + optional: true + + '@ai-sdk/provider@1.0.10': + resolution: {integrity: sha512-pco8Zl9U0xwXI+nCLc0woMtxbvjU8hRmGTseAUiPHFLYAAL8trRPCukg69IDeinOvIeo1SmXxAIdWWPZOLb4Cg==} + engines: {node: '>=18'} + + '@ai-sdk/svelte@1.1.22': + resolution: {integrity: sha512-xtJjvEPHD8GDB1iODvsRLKplZ9CVUN/gT4U+nttuEvjV42qoPXKkrWwjTLEdWKnwTnoHuuI8mnQMje6RhFWtpA==} + engines: {node: '>=18'} + peerDependencies: + svelte: ^3.0.0 || ^4.0.0 || ^5.0.0 + peerDependenciesMeta: + svelte: + optional: true + + '@ai-sdk/ui-utils@1.1.17': + resolution: {integrity: sha512-fCnp/wntZGqPf6tiCmhuQoSDLSBhXoI5DU2JX4As96EO870+jliE6ozvYUwYOZC6Ta2OKAjjWPcSP7HeHX0b+g==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.0.0 + peerDependenciesMeta: + zod: + optional: true + '@ampproject/remapping@2.3.0': resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} @@ -217,18 +254,18 @@ packages: '@analytics/type-utils@0.6.2': resolution: {integrity: sha512-TD+xbmsBLyYy/IxFimW/YL/9L2IEnM7/EoV9Aeh56U64Ify8o27HJcKjo38XY9Tcn0uOq1AX3thkKgvtWvwFQg==} - '@appwrite.io/console@https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@6dbeff0': - resolution: {tarball: https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@6dbeff0} + '@appwrite.io/console@https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@f76aff1': + resolution: {tarball: https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@f76aff1} version: 1.2.1 - '@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@c339db5': - resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@c339db5} + '@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@504c653542de13ba8b4815805806f301df92790a': + resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@504c653542de13ba8b4815805806f301df92790a} version: 1.0.0-next.7 peerDependencies: svelte: ^4.0.0 - '@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@c339db5874e143bc11356abea82e2e964e391ce8': - resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@c339db5874e143bc11356abea82e2e964e391ce8} + '@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@bd21ff7f': + resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@bd21ff7f} version: 1.0.0-next.7 peerDependencies: svelte: ^4.0.0 @@ -242,13 +279,16 @@ packages: '@appwrite.io/pink-legacy@1.0.3': resolution: {integrity: sha512-GGde5fmPhs+s6/3aFeMPc/kKADG/gTFkYQSy6oBN8pK0y0XNCLrZZgBv+EBbdhwdtqVEWXa0X85Mv9w7jcIlwQ==} - '@appwrite.io/pink-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@c339db5': - resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@c339db5} + '@appwrite.io/pink-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@504c653': + resolution: {tarball: https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@504c653} version: 1.0.0-next.85 peerDependencies: react-dom: ^18.0.0 svelte: ^4.0.0 + '@appwrite.io/pink@0.25.0': + resolution: {integrity: sha512-9z4G/iAhjvVpM7tkNqzGqrokst4hvNt3Fkf2jD4W6n8j//EJbQ3mJ0I+5dye5Sry0a33wUanq5DSztqFNYq5zw==} + '@asamuzakjp/css-color@3.1.1': resolution: {integrity: sha512-hpRD68SV2OMcZCsrbdkccTw5FXjNDLo5OuqSHyHZfwweGsDWZwDJ2+gONyNAbazZclobMirACLw0lk8WVxIqxA==} @@ -2071,6 +2111,10 @@ packages: resolution: {integrity: sha512-9jgfSCa3dmEme2ES3mPByGXfgZ87VbP97tng1G2nWwWx6bV2nYxm2AWCrbQjXToSe+yYlqaZNtxffR9IeQr95g==} engines: {node: '>=14.18'} + eventsource-parser@3.0.0: + resolution: {integrity: sha512-T1C0XCUimhxVQzW4zFipdx0SficT651NnkR0ZSH3yQwh+mFMdLfgjABVi4YtMTtaL4s168593DaoaRLMqryavA==} + engines: {node: '>=18.0.0'} + expect-type@1.2.0: resolution: {integrity: sha512-80F22aiJ3GLyVnS/B3HzgR6RelZVumzj9jkL0Rhz4h0xYbNW9PjlQz5h3J/SShErbXBc295vseR4/MIbVmUbeA==} engines: {node: '>=12.0.0'} @@ -2421,6 +2465,9 @@ packages: json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -2616,6 +2663,9 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} + normalize.css@8.0.1: + resolution: {integrity: sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg==} + nwsapi@2.2.20: resolution: {integrity: sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==} @@ -2923,6 +2973,9 @@ packages: scheduler@0.23.2: resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + secure-json-parse@2.7.0: + resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -3485,6 +3538,14 @@ packages: zimmerframe@1.1.2: resolution: {integrity: sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==} + zod-to-json-schema@3.24.5: + resolution: {integrity: sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==} + peerDependencies: + zod: ^3.24.1 + + zod@3.24.3: + resolution: {integrity: sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==} + zrender@5.6.1: resolution: {integrity: sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==} @@ -3495,6 +3556,36 @@ snapshots: '@adobe/css-tools@4.4.2': {} + '@ai-sdk/provider-utils@2.1.11(zod@3.24.3)': + dependencies: + '@ai-sdk/provider': 1.0.10 + eventsource-parser: 3.0.0 + nanoid: 3.3.11 + secure-json-parse: 2.7.0 + optionalDependencies: + zod: 3.24.3 + + '@ai-sdk/provider@1.0.10': + dependencies: + json-schema: 0.4.0 + + '@ai-sdk/svelte@1.1.22(svelte@5.25.3)(zod@3.24.3)': + dependencies: + '@ai-sdk/provider-utils': 2.1.11(zod@3.24.3) + '@ai-sdk/ui-utils': 1.1.17(zod@3.24.3) + optionalDependencies: + svelte: 5.25.3 + transitivePeerDependencies: + - zod + + '@ai-sdk/ui-utils@1.1.17(zod@3.24.3)': + dependencies: + '@ai-sdk/provider': 1.0.10 + '@ai-sdk/provider-utils': 2.1.11(zod@3.24.3) + zod-to-json-schema: 3.24.5(zod@3.24.3) + optionalDependencies: + zod: 3.24.3 + '@ampproject/remapping@2.3.0': dependencies: '@jridgewell/gen-mapping': 0.3.8 @@ -3534,13 +3625,13 @@ snapshots: '@analytics/type-utils@0.6.2': {} - '@appwrite.io/console@https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@6dbeff0': {} + '@appwrite.io/console@https://pkg.pr.new/appwrite/appwrite/@appwrite.io/console@f76aff1': {} - '@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@c339db5(svelte@5.25.3)': + '@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@504c653542de13ba8b4815805806f301df92790a(svelte@5.25.3)': dependencies: svelte: 5.25.3 - '@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@c339db5874e143bc11356abea82e2e964e391ce8(svelte@5.25.3)': + '@appwrite.io/pink-icons-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@bd21ff7f(svelte@5.25.3)': dependencies: svelte: 5.25.3 @@ -3553,9 +3644,9 @@ snapshots: '@appwrite.io/pink-icons': 1.0.0 the-new-css-reset: 1.11.3 - '@appwrite.io/pink-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@c339db5(react-dom@18.3.1(react@18.3.1))(svelte@5.25.3)': + '@appwrite.io/pink-svelte@https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@504c653(react-dom@18.3.1(react@18.3.1))(svelte@5.25.3)': dependencies: - '@appwrite.io/pink-icons-svelte': https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@c339db5874e143bc11356abea82e2e964e391ce8(svelte@5.25.3) + '@appwrite.io/pink-icons-svelte': https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@504c653542de13ba8b4815805806f301df92790a(svelte@5.25.3) '@floating-ui/dom': 1.6.13 '@melt-ui/pp': 0.3.2(@melt-ui/svelte@0.86.6(svelte@5.25.3))(svelte@5.25.3) '@melt-ui/svelte': 0.86.6(svelte@5.25.3) @@ -3569,6 +3660,12 @@ snapshots: svelte-motion: 0.12.2(svelte@5.25.3) svelte-sonner: 0.3.28(svelte@5.25.3) + '@appwrite.io/pink@0.25.0': + dependencies: + '@appwrite.io/pink-icons': 0.25.0 + normalize.css: 8.0.1 + the-new-css-reset: 1.11.3 + '@asamuzakjp/css-color@3.1.1': dependencies: '@csstools/css-calc': 2.1.2(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) @@ -5634,6 +5731,8 @@ snapshots: eventsource-parser@1.0.0: {} + eventsource-parser@3.0.0: {} + expect-type@1.2.0: {} fast-deep-equal@3.1.3: {} @@ -5996,6 +6095,8 @@ snapshots: json-schema-traverse@0.4.1: {} + json-schema@0.4.0: {} + json-stable-stringify-without-jsonify@1.0.1: {} json5@2.2.3: {} @@ -6154,6 +6255,8 @@ snapshots: normalize-path@3.0.0: {} + normalize.css@8.0.1: {} + nwsapi@2.2.20: {} object-inspect@1.13.4: {} @@ -6468,6 +6571,8 @@ snapshots: dependencies: loose-envify: 1.4.0 + secure-json-parse@2.7.0: {} + semver@6.3.1: {} semver@7.7.1: {} @@ -7023,6 +7128,12 @@ snapshots: zimmerframe@1.1.2: {} + zod-to-json-schema@3.24.5(zod@3.24.3): + dependencies: + zod: 3.24.3 + + zod@3.24.3: {} + zrender@5.6.1: dependencies: tslib: 2.3.0 diff --git a/src/lib/actions/analytics.ts b/src/lib/actions/analytics.ts index de4c5f9ec..1655166df 100644 --- a/src/lib/actions/analytics.ts +++ b/src/lib/actions/analytics.ts @@ -6,6 +6,7 @@ import { user } from '$lib/stores/user'; import { ENV, MODE, VARS, isCloud } from '$lib/system'; import { AppwriteException } from '@appwrite.io/console'; import { browser } from '$app/environment'; +import { getReferrerAndUtmSource } from '$lib/helpers/utm'; function plausible(domain: string): AnalyticsPlugin { if (!browser) return { name: 'analytics-plugin-plausible' }; @@ -64,6 +65,8 @@ export function trackEvent(name: string, data: object = null): void { }; } + data = { ...data, ...getReferrerAndUtmSource() }; + if (ENV.DEV || ENV.PREVIEW) { console.debug(`[Analytics] Event ${name} ${path}`, data); } else { diff --git a/src/lib/charts/legend.svelte b/src/lib/charts/legend.svelte index 2e72c1410..47489efb2 100644 --- a/src/lib/charts/legend.svelte +++ b/src/lib/charts/legend.svelte @@ -8,16 +8,23 @@
{#each legendData as { name, value }, index} - {@const formattedValue = typeof value === 'number' ? formatNumberWithCommas(value) : value} + {@const formattedValue = + typeof value === 'number' + ? numberFormat === 'comma' + ? formatNumberWithCommas(value) + : abbreviateNumber(value, decimalsForAbbreviate) + : value} {name} ({formattedValue}) diff --git a/src/lib/commandCenter/panels/ai.svelte b/src/lib/commandCenter/panels/ai.svelte index 586c523d3..9bda77da8 100644 --- a/src/lib/commandCenter/panels/ai.svelte +++ b/src/lib/commandCenter/panels/ai.svelte @@ -7,7 +7,7 @@ import { AvatarInitials, Code, LoadingDots, SvgIcon } from '$lib/components'; import { user } from '$lib/stores/user'; - import { useCompletion } from 'ai/svelte'; + import { useCompletion } from '@ai-sdk/svelte'; import { subPanels } from '../subPanels'; import { isLanguage, type Language } from '$lib/components/code.svelte'; @@ -20,7 +20,8 @@ headers: { 'x-appwrite-project': 'console' }, - credentials: 'include' + credentials: 'include', + streamProtocol: 'text' }); const examples = [ @@ -179,7 +180,7 @@ {#if $isLoading || answer}
-
{getInitials($user.name)}
+
{getInitials($user.name || $user.email)}

{previousQuestion}

diff --git a/src/lib/components/billing/alerts/newDevUpgradePro.svelte b/src/lib/components/billing/alerts/newDevUpgradePro.svelte index 29296189a..ab48d3798 100644 --- a/src/lib/components/billing/alerts/newDevUpgradePro.svelte +++ b/src/lib/components/billing/alerts/newDevUpgradePro.svelte @@ -2,7 +2,7 @@ import { base } from '$app/paths'; import { page } from '$app/state'; import { Click, trackEvent } from '$lib/actions/analytics'; - import { BillingPlan } from '$lib/constants'; + import { BillingPlan, NEW_DEV_PRO_UPGRADE_COUPON } from '$lib/constants'; import { Button } from '$lib/elements/forms'; import { organization } from '$lib/stores/organization'; import { activeHeaderAlert } from '$routes/(console)/store'; @@ -29,7 +29,7 @@ secondary fullWidthMobile class="u-line-height-1" - href={`${base}/apply-credit?code=appw50&org=${$organization.$id}`} + href={`${base}/apply-credit?code=${NEW_DEV_PRO_UPGRADE_COUPON}&org=${$organization.$id}`} on:click={() => { trackEvent(Click.CreditsRedeemClick, { from: 'button', diff --git a/src/lib/components/billing/alerts/paymentFailed.svelte b/src/lib/components/billing/alerts/paymentFailed.svelte new file mode 100644 index 000000000..9646864a9 --- /dev/null +++ b/src/lib/components/billing/alerts/paymentFailed.svelte @@ -0,0 +1,25 @@ + + +{#if $failedInvoice && $failedInvoice.teamId === $organization.$id && isOnProjects} + + To avoid service disruptions in your projects, please verify your payment details and try + again. + + + + +{/if} diff --git a/src/lib/components/billing/couponInput.svelte b/src/lib/components/billing/couponInput.svelte index 23d896a75..85e73411c 100644 --- a/src/lib/components/billing/couponInput.svelte +++ b/src/lib/components/billing/couponInput.svelte @@ -18,7 +18,7 @@ async function addCoupon() { try { - const response = await sdk.forConsole.billing.getCoupon(coupon); + const response = await sdk.forConsole.billing.getCouponAccount(coupon); couponData = response; dispatch('validation', couponData); coupon = null; diff --git a/src/lib/components/billing/discountsApplied.svelte b/src/lib/components/billing/discountsApplied.svelte new file mode 100644 index 000000000..e77bf3ca3 --- /dev/null +++ b/src/lib/components/billing/discountsApplied.svelte @@ -0,0 +1,49 @@ + + +{#if value > 0} + +
+

+ + + {label} + +

+ {#if !fixedCoupon && label.toLowerCase() === 'credits'} + + {/if} +
+ {#if value >= 100} +

Credits applied

+ {:else} + -{formatCurrency(value)} + {/if} +
+{/if} diff --git a/src/lib/components/billing/estimatedTotal.svelte b/src/lib/components/billing/estimatedTotal.svelte new file mode 100644 index 000000000..7e7e84803 --- /dev/null +++ b/src/lib/components/billing/estimatedTotal.svelte @@ -0,0 +1,144 @@ + + +{#if estimation} + + + {#if estimation} + {#each estimation.items ?? [] as item} + {#if item.value > 0} + +

{item.label}

+

{formatCurrency(item.value)}

+
+ {/if} + {/each} + {#each estimation.discounts ?? [] as item} + + {/each} +
+ +

Total due

+

+ {formatCurrency(estimation.grossAmount)} +

+
+ +

+ You'll pay {formatCurrency(estimation.grossAmount)} now. + {#if couponData?.code}Once your credits run out,{:else}Then{/if} you'll be charged + {formatCurrency(estimation.amount)} every 30 days. +

+ {/if} + + + {#if budgetEnabled} +
+ +
+ {/if} +
+
+{/if} diff --git a/src/lib/components/billing/estimatedTotalBox.svelte b/src/lib/components/billing/estimatedTotalBox.svelte index e8adcb489..112d506c1 100644 --- a/src/lib/components/billing/estimatedTotalBox.svelte +++ b/src/lib/components/billing/estimatedTotalBox.svelte @@ -43,7 +43,7 @@ Additional seats ({collaborators?.length}) + >Additional seats ({collaborators?.length ?? 0}) {formatCurrency(extraSeatsCost)} diff --git a/src/lib/components/billing/index.ts b/src/lib/components/billing/index.ts index 266d217d8..1cc70d3d9 100644 --- a/src/lib/components/billing/index.ts +++ b/src/lib/components/billing/index.ts @@ -2,8 +2,9 @@ export { default as PaymentBoxes } from './paymentBoxes.svelte'; export { default as CouponInput } from './couponInput.svelte'; export { default as SelectPaymentMethod } from './selectPaymentMethod.svelte'; export { default as UsageRates } from './usageRates.svelte'; -export { default as EstimatedTotalBox } from './estimatedTotalBox.svelte'; export { default as PlanComparisonBox } from './planComparisonBox.svelte'; export { default as EmptyCardCloud } from './emptyCardCloud.svelte'; export { default as CreditsApplied } from './creditsApplied.svelte'; export { default as PlanSelection } from './planSelection.svelte'; +export { default as EstimatedTotal } from './estimatedTotal.svelte'; +export { default as SelectPlan } from './selectPlan.svelte'; diff --git a/src/lib/components/billing/planExcess.svelte b/src/lib/components/billing/planExcess.svelte index f2f874d5e..02793dc70 100644 --- a/src/lib/components/billing/planExcess.svelte +++ b/src/lib/components/billing/planExcess.svelte @@ -2,19 +2,17 @@ import { calculateExcess, plansInfo, tierToPlan, type Tier } from '$lib/stores/billing'; import { organization } from '$lib/stores/organization'; import { toLocaleDate } from '$lib/helpers/date'; - import { Button } from '$lib/elements/forms'; import { humanFileSize } from '$lib/helpers/sizeConvertion'; import { abbreviateNumber } from '$lib/helpers/numbers'; import { formatNum } from '$lib/helpers/string'; import { onMount } from 'svelte'; - import type { OrganizationUsage } from '$lib/sdk/billing'; + import type { Aggregation } from '$lib/sdk/billing'; import { sdk } from '$lib/stores/sdk'; import { BillingPlan } from '$lib/constants'; import { Alert, Icon, Table, Tooltip } from '@appwrite.io/pink-svelte'; import { IconInfo } from '@appwrite.io/pink-icons-svelte'; export let tier: Tier; - export let members: number; const plan = $plansInfo?.get(tier); let excess: { @@ -24,16 +22,15 @@ executions?: number; members?: number; } = null; - let usage: OrganizationUsage = null; + let aggregation: Aggregation = null; let showExcess = false; onMount(async () => { - usage = await sdk.forConsole.billing.listUsage( + aggregation = await sdk.forConsole.billing.getAggregation( $organization.$id, - $organization.billingCurrentInvoiceDate, - new Date().toISOString() + $organization.billingAggregationId ); - excess = calculateExcess(usage, plan, members); + excess = calculateExcess(aggregation, plan); showExcess = Object.values(excess).some((value) => value > 0); }); @@ -41,22 +38,16 @@ {#if showExcess} - Following payment of your final invoice, your organization will switch to the {tierToPlan( - BillingPlan.FREE - ).name} plan. {#if excess?.members > 0}All team members except the owner will be removed on - that date.{/if} Service disruptions may occur unless resource usage is reduced. - - - - + You will retain access to {tierToPlan($organization.billingPlan).name} plan features until your + billing period ends. After that, + {#if excess?.members > 0} + all team members except the owner will be removed, + {:else} + your organization will be limited to Free plan resources, + {/if} and service disruptions may occur if usage exceeds Free plan limits. @@ -73,7 +64,7 @@ {#if excess?.members} Organization members - {plan.members} members + {plan.addons.seats.limit} members

diff --git a/src/lib/components/billing/selectPlan.svelte b/src/lib/components/billing/selectPlan.svelte new file mode 100644 index 000000000..8aae2f0ac --- /dev/null +++ b/src/lib/components/billing/selectPlan.svelte @@ -0,0 +1,51 @@ + + +{#if billingPlan} +

    + {#each $plansInfo.values() as plan} +
  • + + +
    +

    + {plan.name} + {#if $organization?.billingPlan === plan.$id && !isNewOrg} + Current plan + {/if} +

    +

    + {plan.desc} +

    +

    + {formatCurrency(plan?.price ?? 0)} +

    +
    +
    +
    +
  • + {/each} +
+{/if} diff --git a/src/lib/components/billing/usageRates.svelte b/src/lib/components/billing/usageRates.svelte index e835284c2..4635a1d5a 100644 --- a/src/lib/components/billing/usageRates.svelte +++ b/src/lib/components/billing/usageRates.svelte @@ -43,6 +43,13 @@ ]; $: isFree = org.billingPlan === BillingPlan.FREE; + + // equal or above means unlimited! + $: getCorrectSeatsCountValue = (count: number): string | number => { + // php int max is always larger than js + const exceedsSafeLimit = count >= Number.MAX_SAFE_INTEGER; + return exceedsSafeLimit ? 'Unlimited' : count || 0; + }; @@ -75,10 +82,10 @@ {usage.resource} - {plan[usage.id] || 'Unlimited'} + {getCorrectSeatsCountValue(plan.addons.seats.limit)} - {formatCurrency(plan.addons?.member?.price)}/{usage?.unit} + {formatCurrency(plan.addons?.seats?.price)}/{usage?.unit} {:else} diff --git a/src/lib/components/billing/validateCreditModal.svelte b/src/lib/components/billing/validateCreditModal.svelte index ebdf09acb..130532a7b 100644 --- a/src/lib/components/billing/validateCreditModal.svelte +++ b/src/lib/components/billing/validateCreditModal.svelte @@ -19,7 +19,8 @@ async function addCoupon() { try { - const response = await sdk.forConsole.billing.getCoupon(coupon); + // const response = await sdk.forConsole.billing.getCoupon(coupon); + const response = await sdk.forConsole.billing.getCouponAccount(coupon); //TODO: double check that this is the correct method if (response.onlyNewOrgs && !isNewOrg) { show = false; @@ -37,6 +38,14 @@ message: 'Credits applied successfully' }); } + couponData = response; + dispatch('validation', couponData); + coupon = null; + show = false; + addNotification({ + type: 'success', + message: 'Credits applied successfully' + }); } catch (e) { error = e.message; } diff --git a/src/lib/components/boxAvatar.svelte b/src/lib/components/boxAvatar.svelte index c2400d897..7ba70c789 100644 --- a/src/lib/components/boxAvatar.svelte +++ b/src/lib/components/boxAvatar.svelte @@ -4,7 +4,7 @@ - +
diff --git a/src/lib/components/emptyCardImageCloud.svelte b/src/lib/components/emptyCardImageCloud.svelte index 53f6c2124..2063c3388 100644 --- a/src/lib/components/emptyCardImageCloud.svelte +++ b/src/lib/components/emptyCardImageCloud.svelte @@ -7,13 +7,10 @@ export let noAspectRatio = false; - - + + {#if $$slots?.image} -
+
{/if} diff --git a/src/lib/components/feedback/feedback.svelte b/src/lib/components/feedback/feedback.svelte index 9fc52afd3..2d1a7623c 100644 --- a/src/lib/components/feedback/feedback.svelte +++ b/src/lib/components/feedback/feedback.svelte @@ -11,6 +11,7 @@ import { addNotification } from '$lib/stores/notifications'; import { page } from '$app/state'; import { Typography } from '@appwrite.io/pink-svelte'; + import { project } from '$routes/(console)/project-[project]/store'; $: $selectedFeedback = feedbackOptions.find((option) => option.type === $feedback.type); @@ -24,8 +25,11 @@ page.url.href, $user.name, $user.email, - $organization.billingPlan, - $feedbackData.value + $organization?.billingPlan, + $feedbackData.value, + $organization?.$id, + $project?.$id, + $user.$id ); addNotification({ type: 'success', diff --git a/src/lib/components/filters/customFilters.svelte b/src/lib/components/filters/customFilters.svelte index 09ff475fb..8fd765f27 100644 --- a/src/lib/components/filters/customFilters.svelte +++ b/src/lib/components/filters/customFilters.svelte @@ -12,7 +12,9 @@ { show = true; - }}>Custom filters + }}> + Custom filters + {#if show} diff --git a/src/lib/components/filters/filtersModal.svelte b/src/lib/components/filters/filtersModal.svelte index aebb86f76..a192a60ea 100644 --- a/src/lib/components/filters/filtersModal.svelte +++ b/src/lib/components/filters/filtersModal.svelte @@ -11,11 +11,19 @@ } from '$lib/elements/forms'; import type { Writable } from 'svelte/store'; import Modal from '../modal.svelte'; - import { addFilter, generateTag, operators, queries, type TagValue } from './store'; + import { + addFilter, + generateTag, + operators, + queries, + ValidOperators, + type TagValue + } from './store'; import type { Column } from '$lib/helpers/types'; import { TagList } from '.'; import { Icon, Layout } from '@appwrite.io/pink-svelte'; import { IconPlus } from '@appwrite.io/pink-icons-svelte'; + import { addFilterAndApply } from './quickFilters'; export let show = false; export let columns: Writable; @@ -35,9 +43,17 @@ function apply() { localQueries.forEach((query) => { - addFilter($columns, query.id, query.operator, query.value, query.arrayValues); + addFilterAndApply( + query.id, + $columns.find((c) => c.id === query.id).title, + query.operator as ValidOperators, + query.value, + query.arrayValues, + $columns, + analyticsSource + ); }); - queries.apply(); + localTags = []; localQueries = []; trackEvent(Submit.FilterApply, { @@ -48,7 +64,7 @@ } function addCondition() { - const newTag = generateTag(selectedColumn, operatorKey, value, arrayValues); + const newTag = generateTag(selectedColumn, operatorKey, value || arrayValues); if (localTags.some((t) => t.tag === newTag.tag && t.value === newTag.value)) { return; } else { diff --git a/src/lib/components/filters/quickFilters.svelte b/src/lib/components/filters/quickFilters.svelte index 143fe5c73..c00316154 100644 --- a/src/lib/components/filters/quickFilters.svelte +++ b/src/lib/components/filters/quickFilters.svelte @@ -1,107 +1,70 @@ -{#if $isSmallViewport} - {#if $parsedTags?.length} - - {:else} - - {/if} -{:else} - - {#if $parsedTags?.length} - - {:else} - - {/if} - - {#each filterCols as filter (filter.title + filter.id)} - {#if filter.options} - { - addFilterAndApply( - filter.id, - filter.title, - filter.operator, - e.detail.value, - filter?.array - ? (filter.options - .filter((opt) => opt.checked) - .map((opt) => opt.value) ?? []) - : [], - $columns, - analyticsSource - ); - }} - on:clear={() => { - addFilterAndApply( - filter.id, - filter.title, - filter.operator, - null, - [], - $columns, - analyticsSource - ); - }} /> - {/if} - {/each} - + + + + {#each filterCols.filter((f) => f?.options) as filter (filter.title + filter.id)} + {#if filter.options} + { + addFilterAndApply( + filter.id, + filter.title, + filter.operator, + e.detail.value, + filter?.array + ? (filter.options + .filter((opt) => opt.checked) + .map((opt) => opt.value) ?? []) + : [], + $columns, + analyticsSource + ); + }} + on:clear={() => { + addFilterAndApply( + filter.id, + filter.title, + filter.operator, + null, + [], + $columns, + analyticsSource + ); + }} /> + {/if} + {/each} + - - - - -{/if} - -{#if $isSmallViewport && openBottomSheet} - -{/if} + + + + diff --git a/src/lib/components/filters/quickfiltersSubMenu.svelte b/src/lib/components/filters/quickfiltersSubMenu.svelte index b49bef07e..9b4c3275c 100644 --- a/src/lib/components/filters/quickfiltersSubMenu.svelte +++ b/src/lib/components/filters/quickfiltersSubMenu.svelte @@ -34,9 +34,10 @@
- - {filter.title} + + + {filter.title} +
diff --git a/src/lib/components/filters/setFilters.ts b/src/lib/components/filters/setFilters.ts index 7c8d858d2..c59a2408c 100644 --- a/src/lib/components/filters/setFilters.ts +++ b/src/lib/components/filters/setFilters.ts @@ -9,17 +9,18 @@ export function setFilters(localTags: TagValue[], filterCols: FilterData[], $col if (!localTags?.length) { filterCols.forEach((filter) => { resetOptions(filter); - cleanOldTags(filter.title); + cleanOldTags(filter?.title); }); } else { filterCols.forEach((filter) => { - if (filter.id.toLowerCase().includes('duration')) { + const id = filter?.id?.toLowerCase(); + if (id?.includes('duration')) { setTimeFilter(filter, $columns); - } else if (filter.id.toLocaleLowerCase().includes('size')) { + } else if (id?.includes('size')) { setSizeFilter(filter, $columns); - } else if (filter.id.toLocaleLowerCase().includes('statuscode')) { + } else if (id?.includes('statuscode')) { setStatusCodeFilter(filter, $columns); - } else if (filter.id === '$createdAt' || filter.id === '$updatedAt') { + } else if (id === '$createdat' || id === '$updatedat') { setDateFilter(filter, $columns); } else { setFilterData(filter); @@ -40,7 +41,7 @@ export function setFilterData(filter: FilterData) { option.checked = values.includes(option.value); }); } - cleanOldTags(filter.title); + cleanOldTags(filter?.title); const newTag = { tag: tagData.tag.replace(',', ' or '), value: tagData.value @@ -52,7 +53,7 @@ export function setFilterData(filter: FilterData) { }); } else { resetOptions(filter); - cleanOldTags(filter.title); + cleanOldTags(filter?.title); } } @@ -76,7 +77,7 @@ export function setTimeFilter(filter: FilterData, columns: Column[]) { value: timeTag.value }; - cleanOldTags(filter.title); + cleanOldTags(filter?.title); parsedTags.update((tags) => { tags.push(newTag); @@ -84,7 +85,7 @@ export function setTimeFilter(filter: FilterData, columns: Column[]) { }); } } else { - cleanOldTags(filter.title); + cleanOldTags(filter?.title); } } @@ -102,7 +103,7 @@ export function setSizeFilter(filter: FilterData, columns: Column[]) { return prev; }); if (sizeRange) { - cleanOldTags(filter.title); + cleanOldTags(filter?.title); const newTag = { tag: `**${filter.title}** is **${sizeRange.label}**`, @@ -114,7 +115,7 @@ export function setSizeFilter(filter: FilterData, columns: Column[]) { }); } } else { - cleanOldTags(filter.title); + cleanOldTags(filter?.title); } } @@ -127,7 +128,7 @@ export function setStatusCodeFilter(filter: FilterData, columns: Column[]) { const codeRange = ranges.find((c) => c?.value && c.value === statusCodeTag.value); if (codeRange) { - cleanOldTags(filter.title); + cleanOldTags(filter?.title); const newTag = { tag: `**${filter.title}** is **${codeRange.label}**`, value: statusCodeTag.value @@ -139,7 +140,7 @@ export function setStatusCodeFilter(filter: FilterData, columns: Column[]) { }); } } else { - cleanOldTags(filter.title); + cleanOldTags(filter?.title); } } @@ -158,7 +159,7 @@ export function setDateFilter(filter: FilterData, columns: Column[]) { return prev; }); if (dateRange) { - cleanOldTags(filter.title); + cleanOldTags(filter?.title); const newTag = { tag: `**${filter.title}** is **${dateRange.label}**`, value: dateTag.value @@ -169,11 +170,12 @@ export function setDateFilter(filter: FilterData, columns: Column[]) { }); } } else { - cleanOldTags(filter.title); + cleanOldTags(filter?.title); } } function cleanOldTags(title: string) { + if (!title) return; parsedTags.update((tags) => { tags = tags.filter((tag) => !tag.tag.includes(`**${title}**`)); return tags; @@ -181,6 +183,7 @@ function cleanOldTags(title: string) { } export function resetOptions(filter: FilterData) { + if (!filter?.options) return; filter.options.forEach((option) => { option.checked = false; }); diff --git a/src/lib/components/git/deploymentDomains.svelte b/src/lib/components/git/deploymentDomains.svelte index bb6ed3942..f588cc57c 100644 --- a/src/lib/components/git/deploymentDomains.svelte +++ b/src/lib/components/git/deploymentDomains.svelte @@ -42,12 +42,12 @@ {#if sortedDomains.length > 1} - + +{sortedDomains.length - 1} - + {#each sortedDomains as rule, i} {#if i !== 0} - import { EmptySearch, Paginator } from '$lib/components'; + import { Empty, EmptySearch, Paginator } from '$lib/components'; import { Button, InputSearch, InputSelect } from '$lib/elements/forms'; import { timeFromNow } from '$lib/helpers/date'; import { sdk } from '$lib/stores/sdk'; @@ -20,6 +20,7 @@ import { VCSDetectionType, type Models } from '@appwrite.io/console'; import { getFrameworkIcon } from '$lib/stores/sites'; import { connectGitHub } from '$lib/stores/git'; + import Card from '../card.svelte'; let { action = $bindable('select'), @@ -79,7 +80,6 @@ selectedRepository = $repositories.repositories[0].id; $repository = $repositories.repositories[0]; } - return $repositories.repositories; } @@ -148,7 +148,10 @@ ); }} bind:value={selectedInstallation} /> - + {/await} @@ -173,7 +176,10 @@ {:then response} {#if response?.length} - {#snippet children(paginatedItems)} + {#snippet children( + paginatedItems: Models.ProviderRepositoryRuntime[] & + Models.ProviderRepositoryFramework[] + )} {#each paginatedItems as repo} @@ -257,7 +263,7 @@ {/snippet} - {:else} + {:else if search} {#if search} @@ -267,6 +273,14 @@ {/if} + {:else} + + + + No repositories available + + + {/if} {/await} {/if} diff --git a/src/lib/components/labelCard.svelte b/src/lib/components/labelCard.svelte index 052e2ddd3..ffcd8474c 100644 --- a/src/lib/components/labelCard.svelte +++ b/src/lib/components/labelCard.svelte @@ -1,5 +1,5 @@ - + diff --git a/src/lib/components/paginator.svelte b/src/lib/components/paginator.svelte index e7b252d82..de410b087 100644 --- a/src/lib/components/paginator.svelte +++ b/src/lib/components/paginator.svelte @@ -1,7 +1,8 @@ - - + {@render children(paginatedItems, limit)} {#if !hideFooter} diff --git a/src/lib/components/permissions/permissions.svelte b/src/lib/components/permissions/permissions.svelte index 0cab252ed..efefa7f1a 100644 --- a/src/lib/components/permissions/permissions.svelte +++ b/src/lib/components/permissions/permissions.svelte @@ -229,3 +229,14 @@
{/if} + + diff --git a/src/lib/components/progressBarBig.svelte b/src/lib/components/progressBarBig.svelte index 54ed44d8c..c521e7f95 100644 --- a/src/lib/components/progressBarBig.svelte +++ b/src/lib/components/progressBarBig.svelte @@ -7,7 +7,7 @@ export let maxValue: string | undefined = undefined; export let maxUnit: string | undefined = undefined; export let progressValue: number; - export let progressMax: number; + export let progressMax: number | undefined = undefined; export let showBar = true; export let progressBarData: Array = []; @@ -15,7 +15,7 @@
- {#if currentValue !== undefined && currentUnit !== undefined && progress !== undefined && maxValue !== undefined} + {#if currentValue !== undefined && currentUnit !== undefined && progress !== undefined}
@@ -23,14 +23,18 @@ {currentValue} {currentUnit} - - {maxValue} - {maxUnit ? maxUnit : ''} - + {#if maxValue !== undefined} + + {maxValue} + {maxUnit ? maxUnit : ''} + + {/if} -
- -
+ {#if progressMax !== undefined} +
+ +
+ {/if}
{/if} diff --git a/src/lib/components/regionCard.svelte b/src/lib/components/regionCard.svelte index 3e1964802..795451e46 100644 --- a/src/lib/components/regionCard.svelte +++ b/src/lib/components/regionCard.svelte @@ -1,9 +1,12 @@
diff --git a/src/lib/layout/usageMultiple.svelte b/src/lib/layout/usageMultiple.svelte index 4b5db9588..421b0892b 100644 --- a/src/lib/layout/usageMultiple.svelte +++ b/src/lib/layout/usageMultiple.svelte @@ -13,6 +13,7 @@ export let count: Models.Metric[][]; export let legendData: LegendData[]; export let showHeader: boolean = true; + export let legendNumberFormat: 'comma' | 'abbreviate' = 'comma';
@@ -55,7 +56,10 @@ }))} /> {#if legendData} - + {/if}
{/if} diff --git a/src/lib/layout/wizardSecondaryContent.svelte b/src/lib/layout/wizardSecondaryContent.svelte index 6afa25335..f4e9ea882 100644 --- a/src/lib/layout/wizardSecondaryContent.svelte +++ b/src/lib/layout/wizardSecondaryContent.svelte @@ -1,8 +1,12 @@ + +
- {#if $$slots.aside} + {#if !hideSidebar}
diff --git a/src/lib/sdk/billing.ts b/src/lib/sdk/billing.ts index d9ac25147..6dcacb3af 100644 --- a/src/lib/sdk/billing.ts +++ b/src/lib/sdk/billing.ts @@ -1,8 +1,8 @@ -import type { Client, Models } from '@appwrite.io/console'; -import type { Organization, OrganizationList } from '../stores/organization'; -import type { PaymentMethod } from '@stripe/stripe-js'; import type { Tier } from '$lib/stores/billing'; import type { Campaign } from '$lib/stores/campaigns'; +import type { Client, Models } from '@appwrite.io/console'; +import type { PaymentMethod } from '@stripe/stripe-js'; +import type { Organization, OrganizationError, OrganizationList } from '../stores/organization'; export type PaymentMethodData = { $id: string; @@ -65,6 +65,32 @@ export type InvoiceList = { total: number; }; +export type Estimation = { + amount: number; + grossAmount: number; + credits: number; + discount: number; + items: EstimationItem[]; + discounts: EstimationItem[]; + trialDays: number; + trialEndDate: string | undefined; + error: string | undefined; +}; + +export type EstimationItem = { + label: string; + value: number; +}; + +export type EstimationDeleteOrganization = { + amount: number; + grossAmount: number; + credits: number; + discount: number; + items: EstimationItem[]; + unpaidInvoices: Invoice[]; +}; + export type Coupon = { $id: string; code: string; @@ -147,10 +173,12 @@ export type Aggregation = { * Total amount of the invoice. */ amount: number; + additionalMembers: number; + /** * Price for additional members */ - additionalMembers: number; + additionalMemberAmount: number; /** * Total storage usage. */ @@ -175,6 +203,10 @@ export type Aggregation = { * Usage logs for the billing period. */ resources: OrganizationUsage; + /** + * Aggregation billing plan + */ + plan: string; }; export type OrganizationUsage = { @@ -182,11 +214,13 @@ export type OrganizationUsage = { executions: Array; databasesReads: Array; databasesWrites: Array; + imageTransformations: Array; executionsTotal: number; filesStorageTotal: number; buildsStorageTotal: number; databasesReadsTotal: number; databasesWritesTotal: number; + imageTransformationsTotal: number; deploymentsStorageTotal: number; executionsMBSecondsTotal: number; buildsMBSecondsTotal: number; @@ -204,6 +238,7 @@ export type OrganizationUsage = { users: number; authPhoneTotal: number; authPhoneEstimate: number; + imageTransformations: number; }>; authPhoneTotal: number; authPhoneEstimate: number; @@ -264,14 +299,25 @@ export type AdditionalResource = { multiplier?: number; }; +export type PlanAddon = { + supported: boolean; + currency: string; + invoiceDesc: string; + price: number; + limit: number; + value: number; + type: string; +}; + export type Plan = { $id: string; name: string; + desc: string; price: number; order: number; bandwidth: number; storage: number; - members: number; + imageTransformations: number; webhooks: number; users: number; teams: number; @@ -283,7 +329,7 @@ export type Plan = { realtime: number; logs: number; authPhone: number; - addons: { + usage: { bandwidth: AdditionalResource; executions: AdditionalResource; member: AdditionalResource; @@ -291,7 +337,11 @@ export type Plan = { storage: AdditionalResource; users: AdditionalResource; }; + addons: { + seats: PlanAddon; + }; trialDays: number; + budgetCapEnabled: boolean; isAvailable: boolean; selfService: boolean; premiumSupport: boolean; @@ -300,9 +350,10 @@ export type Plan = { backupsEnabled: boolean; backupPolicies: number; emailBranding: boolean; + supportsCredits: boolean; }; -export type PlansInfo = { +export type PlanList = { plans: Plan[]; total: number; }; @@ -337,20 +388,45 @@ export class Billing { ); } + async validateOrganization(organizationId: string, invites: string[]): Promise { + const path = `/organizations/${organizationId}/validate`; + const params = { + organizationId, + invites + }; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call( + 'PATCH', + uri, + { + 'content-type': 'application/json' + }, + params + ); + } + async createOrganization( organizationId: string, name: string, billingPlan: string, paymentMethodId: string, - billingAddressId: string = undefined - ): Promise { + billingAddressId: string = null, + couponId: string = null, + invites: Array = [], + budget: number = undefined, + taxId: string = null + ): Promise { const path = `/organizations`; const params = { organizationId, name, billingPlan, paymentMethodId, - billingAddressId + billingAddressId, + couponId, + invites, + budget, + taxId }; const uri = new URL(this.client.config.endpoint + path); return await this.client.call( @@ -363,6 +439,28 @@ export class Billing { ); } + async estimationCreateOrganization( + billingPlan: string, + couponId: string = null, + invites: Array = [] + ): Promise { + const path = `/organizations/estimations/create-organization`; + const params = { + billingPlan, + couponId, + invites + }; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call( + 'patch', + uri, + { + 'content-type': 'application/json' + }, + params + ); + } + async deleteOrganization(organizationId: string): Promise { const path = `/organizations/${organizationId}`; const params = { @@ -379,20 +477,30 @@ export class Billing { ); } - async getPlan(organizationId: string): Promise { - const path = `/organizations/${organizationId}/plan`; - const params = { - organizationId - }; + async estimationDeleteOrganization( + organizationId: string + ): Promise { + const path = `/organizations/${organizationId}/estimations/delete-organization`; const uri = new URL(this.client.config.endpoint + path); - return await this.client.call( - 'get', - uri, - { - 'content-type': 'application/json' - }, - params - ); + return await this.client.call('patch', uri, { + 'content-type': 'application/json' + }); + } + + async getOrganizationPlan(organizationId: string): Promise { + const path = `/organizations/${organizationId}/plan`; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call('get', uri, { + 'content-type': 'application/json' + }); + } + + async getPlan(planId: string): Promise { + const path = `/console/plans/${planId}`; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call('get', uri, { + 'content-type': 'application/json' + }); } async getRoles(organizationId: string): Promise { @@ -407,14 +515,22 @@ export class Billing { organizationId: string, billingPlan: string, paymentMethodId: string, - billingAddressId: string = undefined - ): Promise { + billingAddressId: string = undefined, + couponId: string = null, + invites: Array = [], + budget: number = undefined, + taxId: string = null + ): Promise { const path = `/organizations/${organizationId}/plan`; const params = { organizationId, billingPlan, paymentMethodId, - billingAddressId + billingAddressId, + couponId, + invites, + budget, + taxId }; const uri = new URL(this.client.config.endpoint + path); return await this.client.call( @@ -427,6 +543,37 @@ export class Billing { ); } + async estimationUpdatePlan( + organizationId: string, + billingPlan: string, + couponId: string = null, + invites: Array = [] + ): Promise { + const path = `/organizations/${organizationId}/estimations/update-plan`; + const params = { + billingPlan, + couponId, + invites + }; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call( + 'patch', + uri, + { + 'content-type': 'application/json' + }, + params + ); + } + + async cancelDowngrade(organizationId: string): Promise { + const path = `/organizations/${organizationId}/plan/cancel`; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call('patch', uri, { + 'content-type': 'application/json' + }); + } + async updateBudget( organizationId: string, budget: number, @@ -594,6 +741,14 @@ export class Billing { ); } + async updateInvoiceStatus(organizationId: string, invoiceId: string): Promise { + const path = `/organizations/${organizationId}/invoices/${invoiceId}/status`; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call('PATCH', uri, { + 'content-type': 'application/json' + }); + } + async retryPayment( organizationId: string, invoiceId: string, @@ -750,6 +905,22 @@ export class Billing { } } + async getCouponAccount(couponId: string): Promise { + const path = `/account/coupons/${couponId}`; + const params = { + couponId + }; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call( + 'GET', + uri, + { + 'content-type': 'application/json' + }, + params + ); + } + async getCoupon(couponId: string): Promise { const path = `/console/coupons/${couponId}`; const params = { @@ -1120,7 +1291,7 @@ export class Billing { ); } - async getPlansInfo(): Promise { + async getPlansInfo(): Promise { const path = `/console/plans`; const params = {}; const uri = new URL(this.client.config.endpoint + path); diff --git a/src/lib/sdk/usage.ts b/src/lib/sdk/usage.ts index 304bad1f6..b446bfa50 100644 --- a/src/lib/sdk/usage.ts +++ b/src/lib/sdk/usage.ts @@ -185,6 +185,14 @@ export type UsageBuckets = { * Aggregated statistics of bucket storage files per period. */ storage: Metric[]; + /** + * Aggregated statistics of bucket image transformations per period. + */ + imageTransformations: Metric[]; + /** + * Total aggregated number of bucket image transformations. + */ + imageTransformationsTotal: number; }; /** * UsageFunctions @@ -311,7 +319,17 @@ export type UsageProject = { authPhoneEstimate: number; /** - * Aggregated statistics of total number SMS by country + * Aggregated statistics of total number SMS by country. */ authPhoneCountriesBreakdown: Models.MetricBreakdown[]; + + /** + * Array of image transformations per period. + */ + imageTransformations: Metric[]; + + /** + * Aggregated statistics of total number of image transformations. + */ + imageTransformationsTotal: number; }; diff --git a/src/lib/stores/billing.ts b/src/lib/stores/billing.ts index 146239b36..efcd47ba9 100644 --- a/src/lib/stores/billing.ts +++ b/src/lib/stores/billing.ts @@ -1,40 +1,41 @@ -import { page } from '$app/stores'; -import { derived, get, writable } from 'svelte/store'; -import { sdk } from './sdk'; -import { organization, type Organization } from './organization'; -import type { - InvoiceList, - AddressesList, - Invoice, - PaymentList, - PlansMap, - PaymentMethodData, - OrganizationUsage, - Plan -} from '$lib/sdk/billing'; -import { isCloud } from '$lib/system'; -import { cachedStore } from '$lib/helpers/cache'; -import { Query } from '@appwrite.io/console'; -import { headerAlert } from './headerAlert'; -import PaymentAuthRequired from '$lib/components/billing/alerts/paymentAuthRequired.svelte'; -import { addNotification, notifications } from './notifications'; +import { browser } from '$app/environment'; import { goto } from '$app/navigation'; import { base } from '$app/paths'; -import { activeHeaderAlert, orgMissingPaymentMethod } from '$routes/(console)/store'; -import MarkedForDeletion from '$lib/components/billing/alerts/markedForDeletion.svelte'; -import { BillingPlan } from '$lib/constants'; -import PaymentMandate from '$lib/components/billing/alerts/paymentMandate.svelte'; -import MissingPaymentMethod from '$lib/components/billing/alerts/missingPaymentMethod.svelte'; -import LimitReached from '$lib/components/billing/alerts/limitReached.svelte'; import { Click, trackEvent } from '$lib/actions/analytics'; +import { page } from '$app/stores'; +import LimitReached from '$lib/components/billing/alerts/limitReached.svelte'; +import MarkedForDeletion from '$lib/components/billing/alerts/markedForDeletion.svelte'; +import MissingPaymentMethod from '$lib/components/billing/alerts/missingPaymentMethod.svelte'; import newDevUpgradePro from '$lib/components/billing/alerts/newDevUpgradePro.svelte'; -import { last } from '$lib/helpers/array'; +import PaymentAuthRequired from '$lib/components/billing/alerts/paymentAuthRequired.svelte'; +import PaymentMandate from '$lib/components/billing/alerts/paymentMandate.svelte'; +import { BillingPlan, NEW_DEV_PRO_UPGRADE_COUPON } from '$lib/constants'; +import { cachedStore } from '$lib/helpers/cache'; import { sizeToBytes, type Size } from '$lib/helpers/sizeConvertion'; -import { user } from './user'; -import { browser } from '$app/environment'; +import type { + AddressesList, + Aggregation, + Invoice, + InvoiceList, + PaymentList, + PaymentMethodData, + Plan, + PlansMap +} from '$lib/sdk/billing'; +import { isCloud } from '$lib/system'; +import { activeHeaderAlert, orgMissingPaymentMethod } from '$routes/(console)/store'; +import { Query } from '@appwrite.io/console'; +import { derived, get, writable } from 'svelte/store'; +import { headerAlert } from './headerAlert'; +import { addNotification, notifications } from './notifications'; +import { organization, type Organization, type OrganizationError } from './organization'; import { canSeeBilling } from './roles'; +import { sdk } from './sdk'; +import { user } from './user'; +import BudgetLimitAlert from '$routes/(console)/organization-[organization]/budgetLimitAlert.svelte'; +import TeamReadonlyAlert from '$routes/(console)/organization-[organization]/teamReadonlyAlert.svelte'; -export type Tier = 'tier-0' | 'tier-1' | 'tier-2' | 'auto-1' | 'cont-1'; +export type Tier = 'tier-0' | 'tier-1' | 'tier-2' | 'auto-1' | 'cont-1' | 'ent-1'; export const roles = [ { @@ -59,12 +60,20 @@ export const roles = [ } ]; +export const teamStatusReadonly = 'readonly'; +export const billingLimitOutstandingInvoice = 'outstanding_invoice'; + export const paymentMethods = derived(page, ($page) => $page.data.paymentMethods as PaymentList); export const addressList = derived(page, ($page) => $page.data.addressList as AddressesList); export const plansInfo = derived(page, ($page) => $page.data.plansInfo as PlansMap); export const daysLeftInTrial = writable(0); export const readOnly = writable(false); +export const showBudgetAlert = derived( + page, + ($page) => ($page.data.organization?.billingLimits.budgetLimit ?? 0) >= 100 +); + export function getRoleLabel(role: string) { return roles.find((r) => r.value === role)?.label ?? role; } @@ -81,6 +90,8 @@ export function tierToPlan(tier: Tier) { return tierGitHubEducation; case BillingPlan.CUSTOM: return tierCustom; + case BillingPlan.ENTERPRISE: + return tierEnterprise; default: return tierFree; } @@ -130,7 +141,8 @@ export type PlanServices = | 'usersAddon' | 'webhooks' | 'sites' - | 'authPhone'; + | 'authPhone' + | 'imageTransformations'; export function getServiceLimit(serviceId: PlanServices, tier: Tier = null, plan?: Plan): number { if (!isCloud) return 0; @@ -138,6 +150,14 @@ export function getServiceLimit(serviceId: PlanServices, tier: Tier = null, plan const info = get(plansInfo); if (!info) return 0; plan ??= info.get(tier ?? get(organization)?.billingPlan); + // members are no longer a variable on plan itself! + // the correct info for members/seats, resides in `addons`. + // plan > addons > seats/others + if (serviceId === 'members') { + // some don't include `limit`, so we fallback! + return plan?.['addons']['seats']['limit'] ?? 1; + } + return plan?.[serviceId] ?? 0; } @@ -151,11 +171,12 @@ export const failedInvoice = cachedStore< load: async (orgId) => { if (!isCloud) set(null); if (!get(canSeeBilling)) set(null); - const invoices = await sdk.forConsole.billing.listInvoices(orgId); - const failedInvoices = invoices.invoices.filter((i) => i.status === 'failed'); + const failedInvoices = await sdk.forConsole.billing.listInvoices(orgId, [ + Query.equal('status', 'failed') + ]); // const failedInvoices = invoices.invoices; - if (failedInvoices?.length > 0) { - const firstFailed = failedInvoices[0]; + if (failedInvoices?.invoices?.length > 0) { + const firstFailed = failedInvoices.invoices[0]; const today = new Date(); const thirtyDaysAgo = new Date(today.setDate(today.getDate() - 30)); const failedDate = new Date(firstFailed.$createdAt); @@ -200,6 +221,11 @@ export const tierCustom: TierData = { description: 'Team on a custom contract' }; +export const tierEnterprise: TierData = { + name: 'Enterprise', + description: 'For enterprises that need more power and premium support.' +}; + export const showUsageRatesModal = writable(false); export function checkForUsageFees(plan: Tier, id: PlanServices) { @@ -257,13 +283,35 @@ export function calculateTrialDay(org: Organization) { } export async function checkForUsageLimit(org: Organization) { - if (org?.billingPlan !== BillingPlan.FREE) { + if (org?.status === teamStatusReadonly && org?.remarks === billingLimitOutstandingInvoice) { + headerAlert.add({ + id: 'teamReadOnlyFailedInvoices', + component: TeamReadonlyAlert, + show: true, + importance: 11 + }); + readOnly.set(true); + return; + } + if (!org?.billingLimits && org?.status !== teamStatusReadonly) { readOnly.set(false); return; } - if (!org?.billingLimits) { - readOnly.set(false); - return; + if (org?.billingPlan !== BillingPlan.FREE) { + const { budgetLimit } = org?.billingLimits ?? {}; + + if (budgetLimit && budgetLimit >= 100) { + readOnly.set(false); + headerAlert.add({ + id: 'budgetLimit', + component: BudgetLimitAlert, + show: true, + importance: 10 + }); + + readOnly.set(true); + return; + } } const { bandwidth, documents, executions, storage, users } = org?.billingLimits ?? {}; const resources = [ @@ -276,7 +324,8 @@ export async function checkForUsageLimit(org: Organization) { const members = org.total; const plan = get(plansInfo)?.get(org.billingPlan); - const membersOverflow = members > plan.members ? members - (plan.members || members) : 0; + const membersOverflow = + members - 1 > plan.addons.seats.limit ? members - (plan.addons.seats.limit || members) : 0; if (resources.some((r) => r.value >= 100) || membersOverflow > 0) { readOnly.set(true); @@ -456,30 +505,36 @@ export async function checkForNewDevUpgradePro(org: Organization) { if (now - accountCreated < 1000 * 60 * 60 * 24 * 7) return; const isDismissed = !!localStorage.getItem('newDevUpgradePro'); if (isDismissed) return; - if (now - accountCreated < 1000 * 60 * 60 * 24 * 37) { - headerAlert.add({ - id: 'newDevUpgradePro', - component: newDevUpgradePro, - show: true, - importance: 1 - }); + // check if coupon already applied + try { + await sdk.forConsole.billing.getCouponAccount(NEW_DEV_PRO_UPGRADE_COUPON); + } catch (e) { + return; } + headerAlert.add({ + id: 'newDevUpgradePro', + component: newDevUpgradePro, + show: true, + importance: 1 + }); } export const upgradeURL = derived( page, ($page) => `${base}/organization-${$page.data?.organization?.$id}/change-plan` ); +export const billingURL = derived( + page, + ($page) => `${base}/organization-${$page.data?.organization?.$id}/billing` +); export const hideBillingHeaderRoutes = ['/console/create-organization', '/console/account']; -export function calculateExcess(usage: OrganizationUsage, plan: Plan, members: number) { - const totBandwidth = usage?.bandwidth?.length > 0 ? last(usage.bandwidth).value : 0; +export function calculateExcess(addon: Aggregation, plan: Plan) { return { - bandwidth: calculateResourceSurplus(totBandwidth, plan.bandwidth), - storage: calculateResourceSurplus(usage?.storageTotal, plan.storage, 'GB'), - users: calculateResourceSurplus(usage?.usersTotal, plan.users), - executions: calculateResourceSurplus(usage?.executionsTotal, plan.executions, 'GB'), - members: calculateResourceSurplus(members, plan.members) + bandwidth: calculateResourceSurplus(addon.usageBandwidth, plan.bandwidth), + storage: calculateResourceSurplus(addon.usageStorage, plan.storage, 'GB'), + executions: calculateResourceSurplus(addon.usageExecutions, plan.executions, 'GB'), + members: addon.additionalMembers }; } @@ -488,3 +543,7 @@ export function calculateResourceSurplus(total: number, limit: number, limitUnit const realLimit = (limitUnit ? sizeToBytes(limit, limitUnit) : limit) || Infinity; return total > realLimit ? total - realLimit : 0; } + +export function isOrganization(org: Organization | OrganizationError): org is Organization { + return (org as Organization).$id !== undefined; +} diff --git a/src/lib/stores/database.ts b/src/lib/stores/database.ts index efec8fed1..7ce2f1697 100644 --- a/src/lib/stores/database.ts +++ b/src/lib/stores/database.ts @@ -1,10 +1,12 @@ -import { derived, writable } from 'svelte/store'; +import { derived, get, writable } from 'svelte/store'; import { page } from '$app/stores'; import { type Models, Query } from '@appwrite.io/console'; import { sdk } from '$lib/stores/sdk'; import { headerAlert } from '$lib/stores/headerAlert'; import BackupDatabase from '$lib/components/backupDatabaseAlert.svelte'; import { shouldShowNotification } from '$lib/helpers/notifications'; +import { isCloud } from '$lib/system'; +import { currentPlan } from '$lib/stores/organization'; export const database = derived(page, ($page) => $page.data?.database as Models.Database); @@ -17,16 +19,19 @@ export async function checkForDatabaseBackupPolicies(database: Models.Database) if (!shouldShowNotification(backupsBannerId)) return; let total = 0; + const backupsEnabled = get(currentPlan)?.backupsEnabled ?? true; - try { - const policies = await sdk.forProject.backups.listPolicies([ - Query.limit(1), - Query.equal('resourceId', database.$id) - ]); + if (isCloud && backupsEnabled) { + try { + const policies = await sdk.forProject.backups.listPolicies([ + Query.limit(1), + Query.equal('resourceId', database.$id) + ]); - total = policies.total; - } catch (e) { - // ignore, backups not allowed on free plan error. + total = policies.total; + } catch (e) { + // ignore, backups not allowed on free plan error. + } } showPolicyAlert.set(total <= 0); diff --git a/src/lib/stores/feedback.ts b/src/lib/stores/feedback.ts index f23cf953b..62b14f2f8 100644 --- a/src/lib/stores/feedback.ts +++ b/src/lib/stores/feedback.ts @@ -115,10 +115,26 @@ function createFeedbackStore() { email?: string, // eslint-disable-next-line billingPlan?: string, - value?: number + value?: number, + orgId?: string, + projectId?: string, + userId?: string ) => { if (!VARS.GROWTH_ENDPOINT) return; trackEvent(Submit.FeedbackSubmit); + + const customFields: Array<{ id: string; value: string | number }> = [ + { id: '47364', value: currentPage } + ]; + + if (value) { + customFields.push({ id: '40655', value }); + } + + if (billingPlan) { + customFields.push({ id: '56109', value: billingPlan }); + } + const response = await fetch(`${VARS.GROWTH_ENDPOINT}/feedback`, { method: 'POST', headers: { @@ -128,14 +144,13 @@ function createFeedbackStore() { subject, message, email, - // billingPlan, + customFields, firstname: name || 'Unknown', - customFields: [ - { id: '47364', value: currentPage }, - ...(value ? [{ id: '40655', value }] : []) - ], metaFields: { - source: get(feedback).source + source: get(feedback).source, + orgId, + projectId, + userId } }) }); diff --git a/src/lib/stores/oauth-providers.ts b/src/lib/stores/oauth-providers.ts index 5a437dd8b..858b31083 100644 --- a/src/lib/stores/oauth-providers.ts +++ b/src/lib/stores/oauth-providers.ts @@ -101,6 +101,12 @@ export const oAuthProviders: Record = { docs: 'https://developers.facebook.com/', component: Main }, + figma: { + name: 'Figma', + icon: 'figma', + docs: 'https://www.figma.com/developers/api#access-tokens', + component: Main + }, github: { name: 'GitHub', icon: 'github', diff --git a/src/lib/stores/organization.ts b/src/lib/stores/organization.ts index 128962b73..471c25d21 100644 --- a/src/lib/stores/organization.ts +++ b/src/lib/stores/organization.ts @@ -4,6 +4,15 @@ import type { Models } from '@appwrite.io/console'; import type { Tier } from './billing'; import type { Plan } from '$lib/sdk/billing'; +export type OrganizationError = { + status: number; + message: string; + teamId: string; + invoiceId: string; + clientSecret: string; + type: string; +}; + export type Organization = Models.Team> & { billingBudget: number; billingPlan: Tier; @@ -21,6 +30,10 @@ export type Organization = Models.Team> & { amount: number; billingTaxId?: string; billingPlanDowngrade?: Tier; + billingAggregationId: string; + billingInvoiceId: string; + status: string; + remarks: string; }; export type OrganizationList = { @@ -34,6 +47,7 @@ export type BillingLimits = { executions: number; storage: number; users: number; + budgetLimit: number; }; export const newOrgModal = writable(false); diff --git a/src/routes/(console)/account/deleteAccount.svelte b/src/routes/(console)/account/deleteAccount.svelte index e87837f82..0c31d74c4 100644 --- a/src/routes/(console)/account/deleteAccount.svelte +++ b/src/routes/(console)/account/deleteAccount.svelte @@ -14,10 +14,10 @@ - + - {$user.name} + {$user.name || 'User'} diff --git a/src/routes/(console)/account/payments/editAddressModal.svelte b/src/routes/(console)/account/payments/editAddressModal.svelte index cbd2406c2..6697c3ea5 100644 --- a/src/routes/(console)/account/payments/editAddressModal.svelte +++ b/src/routes/(console)/account/payments/editAddressModal.svelte @@ -22,10 +22,6 @@ onMount(async () => { const countryList = await sdk.forProject.locale.listCountries(); - const locale = await sdk.forProject.locale.get(); - if (locale.countryCode) { - selectedAddress.country = locale.countryCode; - } options = countryList.countries.map((country) => { return { value: country.code, @@ -57,6 +53,21 @@ trackError(e, Submit.BillingAddressUpdate); } } + + /** + * This component isn't a modal, so it mounts with its parent. + * On mount, `selectedAddress` can be `null`, so we set the country when the modal shows. + * + * And we only run this if the selected address has no country set, + * which really shouldn't happen, but here we are, playing it safe! + */ + $: if (show && !selectedAddress.country) { + sdk.forProject.locale.get().then((locale) => { + if (locale.countryCode) { + selectedAddress.country = locale.countryCode; + } + }); + } diff --git a/src/routes/(console)/account/updateName.svelte b/src/routes/(console)/account/updateName.svelte index 9f70fd598..07e85def1 100644 --- a/src/routes/(console)/account/updateName.svelte +++ b/src/routes/(console)/account/updateName.svelte @@ -12,7 +12,7 @@ let name: string = null; onMount(async () => { - name ??= $user.name; + name ??= $user.name ?? ''; }); async function updateName() { diff --git a/src/routes/(console)/apply-credit/+page.svelte b/src/routes/(console)/apply-credit/+page.svelte index 5fd6b396f..cdc813d09 100644 --- a/src/routes/(console)/apply-credit/+page.svelte +++ b/src/routes/(console)/apply-credit/+page.svelte @@ -3,25 +3,27 @@ import { base } from '$app/paths'; import { page } from '$app/state'; import { Submit, trackError, trackEvent } from '$lib/actions/analytics'; - import { - CreditsApplied, - EstimatedTotalBox, - SelectPaymentMethod - } from '$lib/components/billing'; + import { CreditsApplied, EstimatedTotal, SelectPaymentMethod } from '$lib/components/billing'; import { BillingPlan, Dependencies } from '$lib/constants'; import { Button, Form, InputSelect, InputTags, InputText } from '$lib/elements/forms'; import { toLocaleDate } from '$lib/helpers/date'; import { Wizard } from '$lib/layout'; - import { type PaymentList } from '$lib/sdk/billing'; + import { type PaymentList, type Plan } from '$lib/sdk/billing'; import { addNotification } from '$lib/stores/notifications'; - import { organizationList, type Organization } from '$lib/stores/organization'; + import { + organizationList, + type Organization, + type OrganizationError + } from '$lib/stores/organization'; import { sdk } from '$lib/stores/sdk'; + import { confirmPayment } from '$lib/stores/stripe.js'; import { ID } from '@appwrite.io/console'; import { onMount } from 'svelte'; import { writable } from 'svelte/store'; - import { plansInfo } from '$lib/stores/billing'; + import { isOrganization, plansInfo, type Tier } from '$lib/stores/billing'; import { Fieldset, Icon, Layout, Tooltip } from '@appwrite.io/pink-svelte'; import { IconInfo } from '@appwrite.io/pink-icons-svelte'; + import EstimatedTotalBox from '$lib/components/billing/estimatedTotalBox.svelte'; export let data; @@ -66,13 +68,19 @@ let coupon: string; let couponData = data?.couponData; let campaign = data?.campaign; - let billingPlan = BillingPlan.PRO; + let billingPlan: Tier = BillingPlan.PRO; let tempOrgId = null; + let currentPlan: Plan; $: onlyNewOrgs = (campaign && campaign.onlyNewOrgs) || (couponData && couponData.onlyNewOrgs); $: selectedOrgId = tempOrgId; + function isUpgrade() { + const newPlan = page.data.plansInfo.get(billingPlan); + return currentPlan && newPlan && currentPlan.order < newPlan.order; + } + onMount(async () => { await loadPaymentMethods(); if (!$organizationList?.total || campaign?.onlyNewOrgs) { @@ -89,6 +97,19 @@ if ($organizationList.total > 0 && tempOrgId === null) { tempOrgId = $organizationList.teams[0].$id; } + if (page.url.searchParams.has('type')) { + const type = page.url.searchParams.get('type'); + if (type === 'payment_confirmed') { + const organizationId = page.url.searchParams.get('id'); + collaborators = page.url.searchParams.get('invites').split(','); + + await sdk.forConsole.billing.validateOrganization(organizationId, collaborators); + } + } + + if (!selectedOrgId && $organizationList?.total) { + selectedOrgId = $organizationList.teams[0].$id; + } }); async function loadPaymentMethods() { @@ -103,76 +124,82 @@ async function handleSubmit() { if (!couponForm.checkValidity()) return; + isSubmitting.set(true); try { - let org: Organization; + let org: Organization | OrganizationError; // Create new org if (selectedOrgId === newOrgId) { org = await sdk.forConsole.billing.createOrganization( newOrgId, name, billingPlan, - paymentMethodId + paymentMethodId, + null, + couponData.code ? couponData.code : null, + collaborators, + billingBudget, + taxId ); } + // Upgrade existing org - else if (selectedOrg?.billingPlan !== billingPlan) { + else if (selectedOrg?.billingPlan !== billingPlan && isUpgrade()) { org = await sdk.forConsole.billing.updatePlan( selectedOrg.$id, billingPlan, paymentMethodId, - null + null, + couponData.code ? couponData.code : null, + collaborators ); } - // Existing pro org + // Existing pro org, apply credits 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]); + if (!isOrganization(org) && org.status === 402) { + let clientSecret = org.clientSecret; + let params = new URLSearchParams(); + params.append('type', 'payment_confirmed'); + params.append('org', org.teamId); + for (const [key, value] of page.url.searchParams.entries()) { + if (key !== 'type' && key !== 'id') { + params.append(key, value); + } + } + params.append('invites', collaborators.join(',')); + await confirmPayment( + '', + clientSecret, + paymentMethodId, + '/console/apply-credit?' + params.toString() + ); + org = await sdk.forConsole.billing.validateOrganization(org.teamId, collaborators); } - // 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` - ); + if (isOrganization(org)) { + 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); } - - // 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); addNotification({ type: 'error', message: e.message }); + } finally { + isSubmitting.set(false); } } @@ -197,10 +224,32 @@ (team) => team.$id === selectedOrgId ) as Organization; - $: billingPlan = - selectedOrg?.billingPlan === BillingPlan.SCALE - ? BillingPlan.SCALE - : (campaign?.plan ?? BillingPlan.PRO); + function getBillingPlan(): Tier | undefined { + const campaignPlan = + campaign?.plan && $plansInfo.get(campaign.plan) ? $plansInfo.get(campaign.plan) : null; + const orgPlan = + selectedOrg?.billingPlan && $plansInfo.get(selectedOrg.billingPlan) + ? $plansInfo.get(selectedOrg.billingPlan) + : null; + + if (!campaignPlan || !orgPlan) { + return; + } + + return campaignPlan.order > orgPlan.order ? campaign.plan : selectedOrg?.billingPlan; + } + + $: if (selectedOrg) { + billingPlan = getBillingPlan(); + } + + $: { + if (selectedOrgId) { + (async () => { + currentPlan = await sdk.forConsole.billing.getOrganizationPlan(selectedOrgId); + })(); + } + } @@ -286,7 +335,7 @@ - {#if selectedOrg?.$id && selectedOrg?.billingPlan !== BillingPlan.FREE && selectedOrg?.billingPlan !== BillingPlan.GITHUB_EDUCATION} + {#if selectedOrg?.$id && selectedOrg?.billingPlan === billingPlan}
{ +export const load = async ({ url }) => { // Has promo code if (url.searchParams.has('code')) { let couponData: Coupon; let campaign: Campaign; const code = url.searchParams.get('code'); try { - couponData = await sdk.forConsole.billing.getCoupon(code); + couponData = await sdk.forConsole.billing.getCouponAccount(code); if (couponData.campaign) { campaign = await sdk.forConsole.billing.getCampaign(couponData.campaign); } diff --git a/src/routes/(console)/create-organization/+page.svelte b/src/routes/(console)/create-organization/+page.svelte index 21c6f426e..eebf3d31b 100644 --- a/src/routes/(console)/create-organization/+page.svelte +++ b/src/routes/(console)/create-organization/+page.svelte @@ -4,24 +4,28 @@ import { page } from '$app/state'; import { Submit, trackError, trackEvent } from '$lib/actions/analytics'; import { - EstimatedTotalBox, PlanComparisonBox, PlanSelection, - SelectPaymentMethod + SelectPaymentMethod, + SelectPlan } from '$lib/components/billing'; import ValidateCreditModal from '$lib/components/billing/validateCreditModal.svelte'; import { BillingPlan, Dependencies } from '$lib/constants'; import { Button, Form, InputTags, InputText } from '$lib/elements/forms'; import { Wizard } from '$lib/layout'; - import type { Coupon } from '$lib/sdk/billing'; - import { tierToPlan } from '$lib/stores/billing'; + + import type { Coupon, PaymentList } from '$lib/sdk/billing'; + import { isOrganization, tierToPlan } from '$lib/stores/billing'; import { addNotification } from '$lib/stores/notifications'; - import type { Organization } from '$lib/stores/organization'; + import { type OrganizationError, type Organization } from '$lib/stores/organization'; import { sdk } from '$lib/stores/sdk'; + import { confirmPayment } from '$lib/stores/stripe'; import { ID } from '@appwrite.io/console'; import { IconPlus } from '@appwrite.io/pink-icons-svelte'; import { Divider, Fieldset, Icon, Layout, Link, Typography } from '@appwrite.io/pink-svelte'; import { writable } from 'svelte/store'; + import EstimatedTotalBox from '$lib/components/billing/estimatedTotalBox.svelte'; + import { onMount } from 'svelte'; export let data; @@ -29,13 +33,22 @@ let selectedCoupon: Partial | null = data.coupon; let previousPage: string = base; let showExitModal = false; + let formComponent: Form; let isSubmitting = writable(false); + + let methods: PaymentList; let name: string; - let paymentMethodId: string = - data.paymentMethods.paymentMethods.find((method) => !!method?.last4)?.$id ?? null; + let billingPlan: BillingPlan = BillingPlan.FREE; + let paymentMethodId: string; let collaborators: string[] = []; + let couponData: Partial = { + code: null, + status: null, + credits: null + }; let taxId: string; + let billingBudget: number; let showCreditModal = false; @@ -43,9 +56,73 @@ previousPage = from?.url?.pathname || previousPage; }); + onMount(async () => { + if (page.url.searchParams.has('coupon')) { + const coupon = page.url.searchParams.get('coupon'); + try { + const response = await sdk.forConsole.billing.getCouponAccount(coupon); + couponData = response; + } catch (e) { + couponData = { + code: null, + status: null, + credits: null + }; + } + } + if (page.url.searchParams.has('name')) { + name = page.url.searchParams.get('name'); + } + if (page.url.searchParams.has('plan')) { + const plan = page.url.searchParams.get('plan'); + if (plan && Object.values(BillingPlan).includes(plan as BillingPlan)) { + billingPlan = plan as BillingPlan; + } + } + if ( + data?.hasFreeOrganizations || + (page.url.searchParams.has('type') && page.url.searchParams.get('type') === 'createPro') + ) { + billingPlan = BillingPlan.PRO; + } + if (page.url.searchParams.has('type')) { + const type = page.url.searchParams.get('type'); + if (type === 'payment_confirmed') { + const organizationId = page.url.searchParams.get('id'); + const invites = page.url.searchParams.get('invites').split(','); + await validate(organizationId, invites); + } + } + }); + + async function loadPaymentMethods() { + methods = await sdk.forConsole.billing.listPaymentMethods(); + paymentMethodId = methods.paymentMethods.find((method) => !!method?.last4)?.$id ?? null; + } + + async function validate(organizationId: string, invites: string[]) { + try { + const org = await sdk.forConsole.billing.validateOrganization(organizationId, invites); + if (isOrganization(org)) { + await preloadData(`${base}/console/organization-${org.$id}`); + await goto(`${base}/console/organization-${org.$id}`); + addNotification({ + type: 'success', + message: `${org.name ?? 'Organization'} has been created` + }); + } + } catch (e) { + addNotification({ + type: 'error', + message: e.message + }); + trackError(e, Submit.OrganizationCreate); + } + } + async function create() { try { - let org: Organization; + let org: Organization | OrganizationError; if (selectedPlan === BillingPlan.FREE) { org = await sdk.forConsole.billing.createOrganization( @@ -61,53 +138,49 @@ name, selectedPlan, paymentMethodId, - null + null, + couponData.code ? couponData.code : null, + collaborators, + billingBudget, + taxId ); - //Add budget - if (billingBudget) { - await sdk.forConsole.billing.updateBudget(org.$id, billingBudget, [75]); - } - - //Add coupon - if (selectedCoupon?.code) { - await sdk.forConsole.billing.addCredit(org.$id, selectedCoupon.code); - trackEvent(Submit.CreditRedeem); - } - - //Add collaborators - if (collaborators?.length) { - collaborators.forEach(async (collaborator) => { - await sdk.forConsole.teams.createMembership( - org.$id, - ['developer'], - collaborator, - undefined, - undefined, - `${page.url.origin}${base}/invite` - ); - }); - } - - // Add tax ID - if (taxId) { - await sdk.forConsole.billing.updateTaxId(org.$id, taxId); + if (!isOrganization(org) && org.status === 402) { + let clientSecret = org.clientSecret; + let params = new URLSearchParams(); + params.append('type', 'payment_confirmed'); + params.append('id', org.teamId); + for (const [key, value] of page.url.searchParams.entries()) { + if (key !== 'type' && key !== 'id') { + params.append(key, value); + } + } + params.append('invites', collaborators.join(',')); + await confirmPayment( + '', + clientSecret, + paymentMethodId, + '/console/create-organization?' + params.toString() + ); + await validate(org.teamId, collaborators); } } trackEvent(Submit.OrganizationCreate, { - plan: tierToPlan(selectedPlan)?.name, - budget_cap_enabled: !!billingBudget, + plan: tierToPlan(billingPlan)?.name, + budget_cap_enabled: billingBudget !== null, members_invited: collaborators?.length }); - await invalidate(Dependencies.ACCOUNT); - await preloadData(`${base}/organization-${org.$id}`); - await goto(`${base}/organization-${org.$id}`); - addNotification({ - type: 'success', - message: `${name ?? 'Organization'} has been created` - }); + if (isOrganization(org)) { + await invalidate(Dependencies.ACCOUNT); + await preloadData(`${base}/organization-${org.$id}`); + await goto(`${base}/organization-${org.$id}`); + addNotification({ + type: 'success', + message: `${org.name ?? 'Organization'} has been created` + }); + } } catch (e) { addNotification({ type: 'error', diff --git a/src/routes/(console)/organization-[organization]/+layout.ts b/src/routes/(console)/organization-[organization]/+layout.ts index 650990e86..cd1968ea8 100644 --- a/src/routes/(console)/organization-[organization]/+layout.ts +++ b/src/routes/(console)/organization-[organization]/+layout.ts @@ -27,7 +27,7 @@ export const load: LayoutLoad = async ({ params, depends }) => { const res = await sdk.forConsole.billing.getRoles(params.organization); roles = res.roles; scopes = res.scopes; - currentPlan = await sdk.forConsole.billing.getPlan(params.organization); + currentPlan = await sdk.forConsole.billing.getOrganizationPlan(params.organization); if (scopes.includes('billing.read')) { await failedInvoice.load(params.organization); if (get(failedInvoice)) { diff --git a/src/routes/(console)/organization-[organization]/+page.svelte b/src/routes/(console)/organization-[organization]/+page.svelte index 1fa4c2699..d35713319 100644 --- a/src/routes/(console)/organization-[organization]/+page.svelte +++ b/src/routes/(console)/organization-[organization]/+page.svelte @@ -24,7 +24,6 @@ import { ID, Region } from '@appwrite.io/console'; import { openImportWizard } from '../project-[project]/settings/migrations/(import)'; import { readOnly } from '$lib/stores/billing'; - import type { RegionList } from '$lib/sdk/billing'; import { onMount, type ComponentType } from 'svelte'; import { canWriteProjects } from '$lib/stores/roles'; import { checkPricingRefAndRedirect } from '$lib/helpers/pricingRedirect'; @@ -40,12 +39,13 @@ } from '@appwrite.io/pink-icons-svelte'; import { getPlatformInfo } from '$lib/helpers/platform'; import CreateProjectCloud from './createProjectCloud.svelte'; + import { regions as regionsStore } from '$routes/(console)/organization-[organization]/store'; export let data; - let addOrganization = false; let showCreate = false; let showCreateProjectCloud = false; + let addOrganization = false; async function allServiceDisabled(project: Models.Project) { let disabled = true; @@ -130,21 +130,18 @@ } }; - let regions: RegionList; onMount(async () => { if (isCloud) { - regions = await sdk.forConsole.billing.listRegions(); + const regions = await sdk.forConsole.billing.listRegions(); + regionsStore.set(regions); checkPricingRefAndRedirect(page.url.searchParams); } - - const searchParams = page.url.searchParams; - if (searchParams.has('create-project')) { - handleCreateProject(); - } }); - function findRegion(project: Models.Project & { region?: string }) { - return regions.regions.find((region) => region.$id === project?.region); + function findRegion(project: Models.Project) { + return $regionsStore?.regions?.find( + (region) => region.$id === (project as Models.Project & { region: string }).region + ); } @@ -211,7 +208,7 @@ {/if} - {#if isCloud && regions} + {#if isCloud && $regionsStore?.regions} {@const region = findRegion(project)} {region?.name} @@ -243,5 +240,5 @@ {#if showCreateProjectCloud} - + {/if} diff --git a/src/routes/(console)/organization-[organization]/billing/+page.svelte b/src/routes/(console)/organization-[organization]/billing/+page.svelte index a6fa6b745..1915fb8d7 100644 --- a/src/routes/(console)/organization-[organization]/billing/+page.svelte +++ b/src/routes/(console)/organization-[organization]/billing/+page.svelte @@ -1,7 +1,6 @@ @@ -91,6 +94,11 @@ Upgrade to a Pro plan to manage when you receive billing alerts for your organization. + {:else if !$currentPlan.budgetCapEnabled} + + Budget caps are not supported on your current plan. For more information, please + reach out to your customer success manager. + {:else} {#if alerts.length >= 4} diff --git a/src/routes/(console)/organization-[organization]/billing/budgetCap.svelte b/src/routes/(console)/organization-[organization]/billing/budgetCap.svelte index 2982bc43d..32a0be64a 100644 --- a/src/routes/(console)/organization-[organization]/billing/budgetCap.svelte +++ b/src/routes/(console)/organization-[organization]/billing/budgetCap.svelte @@ -10,13 +10,14 @@ import { sdk } from '$lib/stores/sdk'; import { Alert, Link } from '@appwrite.io/pink-svelte'; import { onMount } from 'svelte'; + import BudgetAlert from './budgetAlert.svelte'; let capActive = false; let budget: number; onMount(() => { budget = $organization?.billingBudget; - capActive = !!$organization?.billingBudget; + capActive = $organization?.billingBudget !== null; }); async function updateBudget() { @@ -26,7 +27,7 @@ budget, $organization.budgetAlerts ); - invalidate(Dependencies.ORGANIZATION); + await invalidate(Dependencies.ORGANIZATION); addNotification({ type: 'success', isHtml: true, @@ -45,7 +46,7 @@ } $: if (!capActive) { - budget = 0; + budget = null; } @@ -64,6 +65,11 @@ target="_blank" rel="noopener noreferrer">view our pricing guide. + {:else if !$currentPlan.budgetCapEnabled} + + Budget caps are not supported on your current plan. For more information, please + reach out to your customer success manager. + {:else} @@ -105,3 +111,5 @@ + + 0} /> diff --git a/src/routes/(console)/organization-[organization]/billing/cancelDowngradeModal.svelte b/src/routes/(console)/organization-[organization]/billing/cancelDowngradeModal.svelte new file mode 100644 index 000000000..44c9c9710 --- /dev/null +++ b/src/routes/(console)/organization-[organization]/billing/cancelDowngradeModal.svelte @@ -0,0 +1,42 @@ + + + +

+ Your organization is set to change to + {tierToPlan($organization?.billingPlanDowngrade).name} + plan on {toLocaleDate($organization.billingNextInvoiceDate)}. Are you sure + you want to cancel this change and keep your current plan? +

+ + + + +
diff --git a/src/routes/(console)/organization-[organization]/billing/paymentHistory.svelte b/src/routes/(console)/organization-[organization]/billing/paymentHistory.svelte index 3371ff4ed..ddf7206a4 100644 --- a/src/routes/(console)/organization-[organization]/billing/paymentHistory.svelte +++ b/src/routes/(console)/organization-[organization]/billing/paymentHistory.svelte @@ -5,7 +5,7 @@ import { toLocaleDate } from '$lib/helpers/date'; import { formatCurrency } from '$lib/helpers/numbers'; import type { Invoice, InvoiceList } from '$lib/sdk/billing'; - import { getApiEndpoint, sdk } from '$lib/stores/sdk'; + import { sdk } from '$lib/stores/sdk'; import { Query } from '@appwrite.io/console'; import { onMount } from 'svelte'; import { trackEvent } from '$lib/actions/analytics'; @@ -28,8 +28,7 @@ IconExternalLink, IconRefresh } from '@appwrite.io/pink-icons-svelte'; - - // let isLoadingInvoices = true; + import { base } from '$app/paths'; let offset = 0; let invoiceList: InvoiceList = { @@ -38,7 +37,6 @@ }; const limit = 5; - const endpoint = getApiEndpoint(); onMount(request); @@ -47,11 +45,13 @@ invoiceList = await sdk.forConsole.billing.listInvoices(page.params.organization, [ Query.limit(limit), Query.offset(offset), - Query.notEqual('from', $organization.billingCurrentInvoiceDate), - Query.notEqual('status', 'pending'), Query.orderDesc('$createdAt') ]); - // isLoadingInvoices = false; + } + + $: if (page.url.searchParams.get('type') === 'validate-invoice') { + window.history.replaceState({}, '', page.url.pathname); + request(); } function retryPayment(invoice: Invoice) { @@ -135,12 +135,12 @@ + href={`${base}/organizations/${page.params.organization}/invoices/${invoice.$id}/view`}> View invoice + href={`${base}/organizations/${page.params.organization}/invoices/${invoice.$id}/download`}> Download PDF {#if status === 'overdue' || status === 'failed'} diff --git a/src/routes/(console)/organization-[organization]/billing/planSummary.svelte b/src/routes/(console)/organization-[organization]/billing/planSummary.svelte index b03133998..c007b40f5 100644 --- a/src/routes/(console)/organization-[organization]/billing/planSummary.svelte +++ b/src/routes/(console)/organization-[organization]/billing/planSummary.svelte @@ -3,14 +3,13 @@ import { CardGrid } from '$lib/components'; import { Button } from '$lib/elements/forms'; import { toLocaleDate } from '$lib/helpers/date'; - import { plansInfo, tierToPlan, upgradeURL } from '$lib/stores/billing'; + import { plansInfo, upgradeURL } from '$lib/stores/billing'; import { organization } from '$lib/stores/organization'; - import type { CreditList, Invoice, Plan } from '$lib/sdk/billing'; + import type { Aggregation, CreditList, Invoice, Plan } from '$lib/sdk/billing'; import { abbreviateNumber, formatCurrency, formatNumberWithCommas } from '$lib/helpers/numbers'; import { humanFileSize } from '$lib/helpers/sizeConvertion'; import { BillingPlan } from '$lib/constants'; import { Click, trackEvent } from '$lib/actions/analytics'; - import { type Models } from '@appwrite.io/console'; import { Accordion, Card, @@ -21,14 +20,15 @@ Typography } from '@appwrite.io/pink-svelte'; import { IconInfo, IconTag } from '@appwrite.io/pink-icons-svelte'; + import CancelDowngradeModel from './cancelDowngradeModal.svelte'; - export let invoices: Array; - export let members: Models.MembershipList; export let currentPlan: Plan; export let creditList: CreditList; + export let currentInvoice: Invoice | undefined = undefined; + export let currentAggregation: Aggregation | undefined = undefined; + + let showCancel: boolean = false; - const currentInvoice: Invoice | undefined = invoices.length > 0 ? invoices[0] : undefined; - const extraMembers = members.total > 1 ? members.total - 1 : 0; const availableCredit = creditList.available; const today = new Date(); const isTrial = @@ -60,15 +60,13 @@ exclude accumulated credits and applicable taxes.

- Billing period: {toLocaleDate($organization?.billingCurrentInvoiceDate)} - {toLocaleDate( - $organization?.billingNextInvoiceDate - )} + Due at: {toLocaleDate($organization?.billingNextInvoiceDate)}

- {tierToPlan($organization?.billingPlan)?.name} plan + {currentPlan.name} plan {isTrial || $organization?.billingPlan === BillingPlan.GITHUB_EDUCATION @@ -79,36 +77,40 @@ - {#if $organization?.billingPlan !== BillingPlan.FREE && $organization?.billingPlan !== BillingPlan.GITHUB_EDUCATION && extraUsage > 0} + {#if currentPlan.budgeting && extraUsage > 0} + badge={(currentAggregation.additionalMembers > 0 + ? currentInvoice.usage.length + 1 + : currentInvoice.usage.length + ).toString()}> {formatCurrency(extraUsage >= 0 ? extraUsage : 0)} - {#if extraMembers} + {#if currentAggregation.additionalMembers} Additional members - {formatCurrency( - extraMembers * - (currentPlan?.addons?.member?.price ?? 0) - )} + + {formatCurrency( + currentAggregation.additionalMemberAmount + )} + - {extraMembers} + {currentAggregation.additionalMembers} {/if} {#if currentInvoice?.usage} {#each currentInvoice.usage as excess, i} - {#if i > 0 || extraMembers} + {#if i > 0 || currentAggregation.additionalMembers} {/if} {#if ['storage', 'bandwidth'].includes(excess.name)} @@ -180,7 +182,7 @@ {/if} - {#if $organization?.billingPlan !== BillingPlan.FREE && availableCredit > 0} + {#if currentPlan.supportsCredits && availableCredit > 0} @@ -188,7 +190,7 @@ >Credits to be applied - {formatCurrency( + -{formatCurrency( Math.min(availableCredit, currentInvoice?.amount ?? 0) )} @@ -245,17 +247,21 @@ {:else}
- + {#if $organization?.billingPlanDowngrade !== null} + + {:else} + + {/if} @@ -265,6 +271,8 @@ {/if} + + diff --git a/src/routes/(console)/project-[project]/overview/platforms/[platform]/web.svelte b/src/routes/(console)/project-[project]/overview/platforms/[platform]/web.svelte index 9e5c41155..a1fdc9b10 100644 --- a/src/routes/(console)/project-[project]/overview/platforms/[platform]/web.svelte +++ b/src/routes/(console)/project-[project]/overview/platforms/[platform]/web.svelte @@ -8,6 +8,7 @@ import { onMount } from 'svelte'; import { project } from '../../../store'; import { platform } from './store'; + import { hostnameRegex } from '$lib/helpers/string'; let hostname: string = null; @@ -28,7 +29,7 @@ await invalidate(Dependencies.PLATFORM); addNotification({ type: 'success', - message: 'Platform Hostname has been updated' + message: 'Platform hostname has been updated' }); } catch (error) { addNotification({ @@ -48,6 +49,8 @@ id="hostname" label="Hostname" bind:value={hostname} + pattern={hostnameRegex} + patternError="Please enter a valid hostname" required placeholder="myapp.com" /> diff --git a/src/routes/(console)/project-[project]/overview/platforms/components/OnboardingPlatformCard.svelte b/src/routes/(console)/project-[project]/overview/platforms/components/OnboardingPlatformCard.svelte index a9d1fc878..446cc6f78 100644 --- a/src/routes/(console)/project-[project]/overview/platforms/components/OnboardingPlatformCard.svelte +++ b/src/routes/(console)/project-[project]/overview/platforms/components/OnboardingPlatformCard.svelte @@ -1,7 +1,8 @@ diff --git a/src/routes/(console)/project-[project]/settings/domains/+page.svelte b/src/routes/(console)/project-[project]/settings/domains/+page.svelte index 5c8b43a11..431a02178 100644 --- a/src/routes/(console)/project-[project]/settings/domains/+page.svelte +++ b/src/routes/(console)/project-[project]/settings/domains/+page.svelte @@ -5,18 +5,21 @@ import { Button } from '$lib/elements/forms'; import Container from '$lib/layout/container.svelte'; import { IconPlus } from '@appwrite.io/pink-icons-svelte'; - import { Card, Empty, Icon, Layout } from '@appwrite.io/pink-svelte'; - import SearchQuery from '$lib/components/searchQuery.svelte'; + import { Card, Empty, Icon } from '@appwrite.io/pink-svelte'; import { app } from '$lib/stores/app'; import { Click, trackEvent } from '$lib/actions/analytics'; import Table from './table.svelte'; + import { ResponsiveContainerHeader } from '$lib/layout'; let { data } = $props(); - - + - - + {#if data.rules.total} diff --git a/src/routes/(console)/project-[project]/settings/domains/add-domain/+page.svelte b/src/routes/(console)/project-[project]/settings/domains/add-domain/+page.svelte index fc11c25ec..ea86dce94 100644 --- a/src/routes/(console)/project-[project]/settings/domains/add-domain/+page.svelte +++ b/src/routes/(console)/project-[project]/settings/domains/add-domain/+page.svelte @@ -5,21 +5,27 @@ import { Wizard } from '$lib/layout'; import { addNotification } from '$lib/stores/notifications'; import { sdk } from '$lib/stores/sdk'; - import { Fieldset, Layout } from '@appwrite.io/pink-svelte'; + import { Fieldset } from '@appwrite.io/pink-svelte'; import { goto, invalidate } from '$app/navigation'; import { Dependencies } from '$lib/constants'; import { writable } from 'svelte/store'; + import { onMount } from 'svelte'; const routeBase = `${base}/project-${page.params.project}/settings/domains`; - //TODO: enable once combo box is fixed // let { data } = $props(); let formComponent: Form; let isSubmitting = $state(writable(false)); - + let showConnectRepo = $state(false); let domainName = $state(''); + onMount(() => { + if (page.url.searchParams.has('domain')) { + domainName = page.url.searchParams.get('domain'); + } + }); + async function addDomain() { try { const rule = await sdk.forProject.proxy.createAPIRule(domainName); @@ -41,27 +47,12 @@
- -
- - -
-
+ diff --git a/src/routes/(console)/project-[project]/settings/domains/add-domain/verify-[domain]/+page.svelte b/src/routes/(console)/project-[project]/settings/domains/add-domain/verify-[domain]/+page.svelte index b70833e06..9606b0f8e 100644 --- a/src/routes/(console)/project-[project]/settings/domains/add-domain/verify-[domain]/+page.svelte +++ b/src/routes/(console)/project-[project]/settings/domains/add-domain/verify-[domain]/+page.svelte @@ -1,7 +1,14 @@ - -
- -
- - {#if isSubDomain} - (selectedTab = 'cname')} - active={selectedTab === 'cname'}> - CNAME - - {/if} - {#if isCloud} - (selectedTab = 'nameserver')} - active={selectedTab === 'nameserver'}> - Nameservers - - {/if} - - -
- {#if selectedTab === 'cname' && isSubDomain} - - {:else if isCloud} - - {/if} - -
-
diff --git a/src/routes/(console)/project-[project]/settings/domains/deleteDomainModal.svelte b/src/routes/(console)/project-[project]/settings/domains/deleteDomainModal.svelte index 3b2657c63..e2e937ff6 100644 --- a/src/routes/(console)/project-[project]/settings/domains/deleteDomainModal.svelte +++ b/src/routes/(console)/project-[project]/settings/domains/deleteDomainModal.svelte @@ -21,7 +21,7 @@ async function deleteDomain() { try { await sdk.forProject.proxy.deleteRule(selectedDomain.$id); - await invalidate(Dependencies.SITES_DOMAINS); + await invalidate(Dependencies.DOMAINS); show = false; addNotification({ type: 'success', @@ -38,11 +38,7 @@ {#if selectedDomain} - Are you sure you want to delete this domain? You will no longer be able to view your - site by visiting: - - - {selectedDomain.domain} + Are you sure you want to delete this domain? {/if} diff --git a/src/routes/(console)/project-[project]/settings/domains/retryDomainModal.svelte b/src/routes/(console)/project-[project]/settings/domains/retryDomainModal.svelte index 5f0845c30..b5f451d85 100644 --- a/src/routes/(console)/project-[project]/settings/domains/retryDomainModal.svelte +++ b/src/routes/(console)/project-[project]/settings/domains/retryDomainModal.svelte @@ -21,7 +21,7 @@ async function retryDomain() { try { await sdk.forProject.proxy.updateRuleVerification(selectedDomain.$id); - await invalidate(Dependencies.SITES_DOMAINS); + await invalidate(Dependencies.DOMAINS); show = false; addNotification({ type: 'success', diff --git a/src/routes/(console)/project-[project]/settings/domains/table.svelte b/src/routes/(console)/project-[project]/settings/domains/table.svelte index 8631c7c55..8529c72dc 100644 --- a/src/routes/(console)/project-[project]/settings/domains/table.svelte +++ b/src/routes/(console)/project-[project]/settings/domains/table.svelte @@ -5,7 +5,15 @@ import { protocol } from '$routes/(console)/store'; import type { Models } from '@appwrite.io/console'; import { IconDotsHorizontal, IconRefresh, IconTrash } from '@appwrite.io/pink-icons-svelte'; - import { ActionMenu, Icon, Layout, Popover, Table, Typography } from '@appwrite.io/pink-svelte'; + import { + ActionMenu, + Badge, + Icon, + Layout, + Popover, + Table, + Typography + } from '@appwrite.io/pink-svelte'; import DeleteDomainModal from './deleteDomainModal.svelte'; import RetryDomainModal from './retryDomainModal.svelte'; import { columns } from './store'; @@ -19,7 +27,6 @@ let showDelete = $state(false); let showRetry = $state(false); let selectedDomain: Models.ProxyRule = $state(null); - let showPreviewDomainModal = $state(false); @@ -39,14 +46,15 @@ {domain.domain} + {#if domain.status !== 'verified'} + + {/if} - {:else if column.id === 'redirectUrl'} - - {domain?.redirectUrl || 'No redirect'} - {domain?.redirectStatusCode ? `(${domain.redirectStatusCode})` : ''} - {:else if column.id === 'deploymentVcsProviderBranch'} - {domain.deploymentVcsProviderBranch || '-'} {/if} {/each} @@ -69,7 +77,6 @@ { - e.preventDefault(); selectedDomain = domain; showRetry = true; toggle(e); @@ -81,12 +88,11 @@ status="danger" leadingIcon={IconTrash} on:click={(e) => { - e.preventDefault(); selectedDomain = domain; showDelete = true; toggle(e); trackEvent(Click.DomainDeleteClick, { - source: 'sites_domain_overview' + source: 'settings_domain_overview' }); }}> Delete diff --git a/src/routes/(console)/project-[project]/settings/migrations/(import)/step1.svelte b/src/routes/(console)/project-[project]/settings/migrations/(import)/step1.svelte index 772842d77..badd138cf 100644 --- a/src/routes/(console)/project-[project]/settings/migrations/(import)/step1.svelte +++ b/src/routes/(console)/project-[project]/settings/migrations/(import)/step1.svelte @@ -5,7 +5,7 @@ import { provider } from '.'; const providers: Record = { - appwrite: 'Appwrite Self-hosted', + appwrite: 'Appwrite', firebase: 'Firebase', supabase: 'Supabase', nhost: 'NHost' diff --git a/src/routes/(console)/project-[project]/settings/migrations/exportModal.svelte b/src/routes/(console)/project-[project]/settings/migrations/exportModal.svelte index f299631c2..0f4658324 100644 --- a/src/routes/(console)/project-[project]/settings/migrations/exportModal.svelte +++ b/src/routes/(console)/project-[project]/settings/migrations/exportModal.svelte @@ -3,8 +3,10 @@ import { Alert, Box, Modal } from '$lib/components'; import { Button, InputText, InputTextarea } from '$lib/elements/forms'; import { getFormData } from '$lib/helpers/form'; - import { feedback } from '$lib/stores/feedback'; + import { feedback, feedbackData } from '$lib/stores/feedback'; + import { organization } from '$lib/stores/organization'; import { sdk } from '$lib/stores/sdk'; + import { user } from '$lib/stores/user'; import { project } from '../../store'; export let show = false; @@ -53,7 +55,18 @@ const { endpoint, feedback: message } = formData; try { - await feedback.submitFeedback(`feedback-${$feedback.type}`, message, page.url.href); + await feedback.submitFeedback( + `feedback-${$feedback.type}`, + message, + page.url.href, + $user.name, + $user.email, + $organization?.billingPlan, + $feedbackData.value, + $organization?.$id, + $project?.$id, + $user.$id + ); } catch (error) { console.error( 'Feedback could not be submitted, but we continue to redirect to do export.' diff --git a/src/routes/(console)/project-[project]/settings/usage/[[invoice]]/+page.svelte b/src/routes/(console)/project-[project]/settings/usage/[[invoice]]/+page.svelte index 8521a3957..a00376dcc 100644 --- a/src/routes/(console)/project-[project]/settings/usage/[[invoice]]/+page.svelte +++ b/src/routes/(console)/project-[project]/settings/usage/[[invoice]]/+page.svelte @@ -27,7 +27,8 @@ data.usage.filesStorageTotal + data.usage.deploymentsStorageTotal + data.usage.buildsStorageTotal; - + $: imageTransformations = data.usage.imageTransformations; + $: imageTransformationsTotal = data.usage.imageTransformationsTotal; $: dbReads = data.usage.databasesReads; $: dbWrites = data.usage.databasesWrites; @@ -181,7 +182,48 @@ ]} /> - + + {:else} + +
+ +

No data to show

+
+
+ {/if} +
+ + + Image transformations + Total unique image transformations in your project. + + {#if imageTransformations} + {@const current = formatNum(imageTransformationsTotal)} +
+
+

+ {current} + Transformations +

+
+
+ [e.date, e.value])] + } + ]} /> {:else} diff --git a/src/routes/(console)/project-[project]/settings/webhooks/+page.svelte b/src/routes/(console)/project-[project]/settings/webhooks/+page.svelte index 275b626c7..e83787f88 100644 --- a/src/routes/(console)/project-[project]/settings/webhooks/+page.svelte +++ b/src/routes/(console)/project-[project]/settings/webhooks/+page.svelte @@ -94,7 +94,7 @@ single href="https://appwrite.io/docs/advanced/platform/webhooks" target="webhook" - on:click={() => goto(`${base}/projects-${projectId}/settings/webhooks/create`)} /> + on:click={() => goto(`${base}/project-${projectId}/settings/webhooks/create`)} /> {/if} diff --git a/src/routes/(console)/project-[project]/sites/(components)/deploymentActionMenu.svelte b/src/routes/(console)/project-[project]/sites/(components)/deploymentActionMenu.svelte index 30c518c31..7cd9df5d1 100644 --- a/src/routes/(console)/project-[project]/sites/(components)/deploymentActionMenu.svelte +++ b/src/routes/(console)/project-[project]/sites/(components)/deploymentActionMenu.svelte @@ -85,7 +85,7 @@ - + { - let f = file as Partial & { removable: boolean }; - f.removable = true; - return f; + ? Array.from(files).map((f) => { + return { + ...f, + extension: f.type, + removable: true + }; }) : []; diff --git a/src/routes/(console)/project-[project]/sites/create-site/repositories/+page.svelte b/src/routes/(console)/project-[project]/sites/create-site/repositories/+page.svelte index 8f1ecc15b..c89960343 100644 --- a/src/routes/(console)/project-[project]/sites/create-site/repositories/+page.svelte +++ b/src/routes/(console)/project-[project]/sites/create-site/repositories/+page.svelte @@ -57,8 +57,9 @@ Missing a repository? - Check your permissions in GitHub, your repository might be set to - private. + Make sure Appwrite has access to your GitHub repositories. If you chose + specific repos, you may need to update your permissions to include the + missing one. diff --git a/src/routes/(console)/project-[project]/sites/create-site/templates/+page.svelte b/src/routes/(console)/project-[project]/sites/create-site/templates/+page.svelte index 4c69e6df5..fcf9802fb 100644 --- a/src/routes/(console)/project-[project]/sites/create-site/templates/+page.svelte +++ b/src/routes/(console)/project-[project]/sites/create-site/templates/+page.svelte @@ -125,7 +125,7 @@
{#if data.templates?.length > 0} - + {#snippet children(paginatedItems: typeof data.templates)} {#each paginatedItems as template (template.name)} diff --git a/src/routes/(console)/project-[project]/sites/create-site/templates/template-[template]/configuration.svelte b/src/routes/(console)/project-[project]/sites/create-site/templates/template-[template]/configuration.svelte index 9687ff888..1d50a71e3 100644 --- a/src/routes/(console)/project-[project]/sites/create-site/templates/template-[template]/configuration.svelte +++ b/src/routes/(console)/project-[project]/sites/create-site/templates/template-[template]/configuration.svelte @@ -9,7 +9,14 @@ InputURL } from '$lib/elements/forms'; import type { Models } from '@appwrite.io/console'; - import { Fieldset, Layout, Popover, Icon, Accordion } from '@appwrite.io/pink-svelte'; + import { + Fieldset, + Layout, + Popover, + Icon, + Accordion, + ActionMenu + } from '@appwrite.io/pink-svelte'; import { IconInfo } from '@appwrite.io/pink-icons-svelte'; import { type Component } from 'svelte'; import { getApiEndpoint } from '$lib/stores/sdk'; @@ -100,8 +107,10 @@ e.preventDefault(); toggle(e); }}> - -

{variable.description}

+ + +

{variable.description}

+
@@ -140,8 +149,10 @@ e.preventDefault(); toggle(e); }}> - -

{variable.description}

+ + +

{variable.description}

+
diff --git a/src/routes/(console)/project-[project]/sites/create-site/variableEditorModal.svelte b/src/routes/(console)/project-[project]/sites/create-site/variableEditorModal.svelte index 891679b1c..9d95f8665 100644 --- a/src/routes/(console)/project-[project]/sites/create-site/variableEditorModal.svelte +++ b/src/routes/(console)/project-[project]/sites/create-site/variableEditorModal.svelte @@ -122,7 +122,11 @@ {#if secretVariables?.length > 0} {secretVariables.length} secret variables are hidden from the editor. Their values will - remain unchanged. Learn more. + remain unchanged. Learn more. {/if} diff --git a/src/routes/(console)/project-[project]/sites/site-[site]/deployments/+page.svelte b/src/routes/(console)/project-[project]/sites/site-[site]/deployments/+page.svelte index 729a55a4f..84bf92f15 100644 --- a/src/routes/(console)/project-[project]/sites/site-[site]/deployments/+page.svelte +++ b/src/routes/(console)/project-[project]/sites/site-[site]/deployments/+page.svelte @@ -75,7 +75,6 @@ - - +
@@ -223,5 +170,5 @@ {/if} {#if showConnectCLI} - + {/if} diff --git a/src/routes/(console)/project-[project]/sites/site-[site]/deployments/cancelDeploymentModal.svelte b/src/routes/(console)/project-[project]/sites/site-[site]/deployments/cancelDeploymentModal.svelte index 0de8f95f4..d3308394f 100644 --- a/src/routes/(console)/project-[project]/sites/site-[site]/deployments/cancelDeploymentModal.svelte +++ b/src/routes/(console)/project-[project]/sites/site-[site]/deployments/cancelDeploymentModal.svelte @@ -36,6 +36,6 @@

Are you sure you want to cancel this deployment?

- + diff --git a/src/routes/(console)/project-[project]/sites/site-[site]/deployments/createCliModal.svelte b/src/routes/(console)/project-[project]/sites/site-[site]/deployments/createCliModal.svelte index bed603486..d6a72f24c 100644 --- a/src/routes/(console)/project-[project]/sites/site-[site]/deployments/createCliModal.svelte +++ b/src/routes/(console)/project-[project]/sites/site-[site]/deployments/createCliModal.svelte @@ -1,24 +1,24 @@ diff --git a/src/routes/(console)/project-[project]/sites/site-[site]/deployments/deploymentMetrics.svelte b/src/routes/(console)/project-[project]/sites/site-[site]/deployments/deploymentMetrics.svelte index a14fb82e1..7e93abd7c 100644 --- a/src/routes/(console)/project-[project]/sites/site-[site]/deployments/deploymentMetrics.svelte +++ b/src/routes/(console)/project-[project]/sites/site-[site]/deployments/deploymentMetrics.svelte @@ -7,6 +7,8 @@ import { onMount } from 'svelte'; import { page } from '$app/state'; import { UsageCard } from '$lib/components'; + import { humanFileSize } from '$lib/helpers/sizeConvertion'; + import { formatTimeDetailed } from '$lib/helpers/timeConversion'; const now = new Date(); const rangeOptions = [ @@ -33,17 +35,17 @@ description: 'Total builds time' }, { - id: 'avgTime', + id: 'buildsTimeAverage', value: null, description: 'Avg. builds time' }, { - id: 'success', + id: 'buildsSuccessTotal', value: null, description: 'Successful' }, { - id: 'failed', + id: 'buildsFailedTotal', value: null, description: 'Failed ' } @@ -63,7 +65,21 @@ try { const usage = await sdk.forProject.sites.getUsage(page.params.site, range); metrics = metrics.map((metric) => { - metric.value = usage[metric.id] ?? '-'; + if (metric.id === 'buildsStorageTotal') { + const size = humanFileSize(usage[metric.id]); + + metric.value = parseInt(size?.value) > 0 ? size.value + size.unit : '-'; + } else if ( + metric.id === 'buildsTimeTotal' || + metric.id === 'buildsTimeAverage' + ) { + const seconds = usage[metric.id] > 0 ? usage[metric.id] / 1000 : 0; + const time = formatTimeDetailed(seconds); + + metric.value = time !== '0s' ? time : '-'; + } else { + metric.value = usage[metric.id] ?? '-'; + } return metric; }); metrics = metrics; diff --git a/src/routes/(console)/project-[project]/sites/site-[site]/domains/+page.svelte b/src/routes/(console)/project-[project]/sites/site-[site]/domains/+page.svelte index 343292e19..50fbd7359 100644 --- a/src/routes/(console)/project-[project]/sites/site-[site]/domains/+page.svelte +++ b/src/routes/(console)/project-[project]/sites/site-[site]/domains/+page.svelte @@ -58,7 +58,7 @@
@@ -55,7 +47,7 @@