diff --git a/.env.example b/.env.example index e490a6ef4..0eca03648 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,5 @@ VITE_APPWRITE_ENDPOINT=http://localhost/v1 VITE_APPWRITE_GROWTH_ENDPOINT= VITE_GA_PROJECT= -VITE_CONSOLE_MODE=self-hosted \ No newline at end of file +VITE_CONSOLE_MODE=self-hosted +VITE_STRIPE_PUBLIC_KEY= diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index aaac70173..865a0bd9e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -3,8 +3,14 @@ name: Tests on: push: branches: [main] + paths-ignore: + - '**/*.md' + - 'static/**/*' pull_request: branches: [main] + paths-ignore: + - '**/*.md' + - 'static/**/*' env: VITE_APPWRITE_ENDPOINT: http://appwrite.test/v1 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..cc41cea9b --- /dev/null +++ b/.prettierignore @@ -0,0 +1,4 @@ +# Ignore files for PNPM, NPM and YARN +pnpm-lock.yaml +package-lock.json +yarn.lock diff --git a/package-lock.json b/package-lock.json index e169510c4..fb1f19fa1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,12 +6,13 @@ "": { "name": "@appwrite/console", "dependencies": { - "@appwrite.io/console": "npm:matej-appwrite-console@0.3.3", - "@appwrite.io/pink": "0.1.0", - "@appwrite.io/pink-icons": "^0.1.0", + "@appwrite.io/console": "^0.5.0", + "@appwrite.io/pink": "0.2.0", + "@appwrite.io/pink-icons": "0.2.0", "@popperjs/core": "^2.11.8", "@sentry/svelte": "^7.66.0", "@sentry/tracing": "^7.66.0", + "@stripe/stripe-js": "^2.2.0", "ai": "^2.2.11", "analytics": "^0.8.9", "dayjs": "^1.11.9", @@ -28,6 +29,8 @@ "web-vitals": "^3.4.0" }, "devDependencies": { + "@melt-ui/pp": "^0.1.4", + "@melt-ui/svelte": "^0.61.2", "@playwright/test": "^1.37.1", "@sveltejs/adapter-static": "^2.0.3", "@sveltejs/kit": "^1.24.0", @@ -46,13 +49,14 @@ "eslint-plugin-svelte": "^2.33.0", "jsdom": "^22.1.0", "kleur": "^4.1.5", - "prettier": "^3.0.3", - "prettier-plugin-svelte": "^3.0.3", + "prettier": "^3.2.2", + "prettier-plugin-svelte": "^3.1.2", "sass": "^1.66.1", "svelte": "^4.2.0", "svelte-check": "^3.5.1", "svelte-jester": "^2.3.2", "svelte-preprocess": "^5.0.4", + "svelte-sequential-preprocessor": "^2.0.1", "tslib": "^2.6.2", "typescript": "^5.2.2", "vite": "^4.4.9", @@ -72,9 +76,9 @@ } }, "node_modules/@adobe/css-tools": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.1.tgz", - "integrity": "sha512-/62yikz7NLScCGAAST5SHdnjaDJQBDq0M2muyRTpf2VQhw6StBg2ALiu73zSJQ4fMVLA+0uBhBHAle7Wg+2kSg==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.2.tgz", + "integrity": "sha512-DA5a1C0gD/pLOvhv33YMrbf2FK3oUzwNl9oOJqE4XVjuEtt6XIakRcsd7eLiOSPkp1kTRQGICTA8cKra/vFbjw==", "dev": true }, "node_modules/@ampproject/remapping": { @@ -155,34 +159,33 @@ "integrity": "sha512-TD+xbmsBLyYy/IxFimW/YL/9L2IEnM7/EoV9Aeh56U64Ify8o27HJcKjo38XY9Tcn0uOq1AX3thkKgvtWvwFQg==" }, "node_modules/@appwrite.io/console": { - "name": "matej-appwrite-console", - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/matej-appwrite-console/-/matej-appwrite-console-0.3.3.tgz", - "integrity": "sha512-kAv4NcW/jGjxgpkhOXCMPbZ5CYQ8fUc0IKhg7kGeShTxRFNVMgonsI1sMhOKHQp+j3yqPt6dYCvWQCPJjUdDVA==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@appwrite.io/console/-/console-0.5.0.tgz", + "integrity": "sha512-WqjVc6lrmOwNGoyAGNx7z/sBJou1EgGbnGn87TuBvfhZ/zcYSdi+Nc010EwGpyUvPeNhL2ci8Jj3eYnelJVKwg==", "dependencies": { "cross-fetch": "3.1.5", "isomorphic-form-data": "2.0.0" } }, "node_modules/@appwrite.io/pink": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@appwrite.io/pink/-/pink-0.1.0.tgz", - "integrity": "sha512-z+4wiTH2PBMrbxvXiNjGQY0hs6ziCbKSb2rK0O/2M3aa9CHN/dBDSTpWCcED8G9BWDkeS5XMVZrQbyrdqMVc/Q==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@appwrite.io/pink/-/pink-0.2.0.tgz", + "integrity": "sha512-ni6O/yPiWkCYQ68PNcbmus6QrPpIEl7xmzKHU2dkV2L19RVio8thtBtT9ckJJ8uBMGsczASWs0fUqOxIvBmtNw==", "dependencies": { - "@appwrite.io/pink-icons": "0.1.0", + "@appwrite.io/pink-icons": "0.2.0", "normalize.css": "^8.0.1", "the-new-css-reset": "^1.11.2" } }, "node_modules/@appwrite.io/pink-icons": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@appwrite.io/pink-icons/-/pink-icons-0.1.0.tgz", - "integrity": "sha512-Y5DmvosdCoBD0oyqKV+ncbXTzQuhX0WOe4N7uonMaz1NaFm3ODaIb/CF77hV2vg86oOQ+f+BirdhfPsBue2pNA==" + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@appwrite.io/pink-icons/-/pink-icons-0.2.0.tgz", + "integrity": "sha512-wThBUGdvpwT0q2fcUapiugfBiB4RFDvTlW6p+U7Dol61mmCpNteFSIoBPiPJzN5015n1v4UEWL/rdOia4CWdBQ==" }, "node_modules/@babel/code-frame": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.4.tgz", - "integrity": "sha512-r1IONyb6Ia+jYR2vvIDhdWdlTGhqbBoFqLTQidzZ4kepUFH15ejXvFHxCVbtl7BOXIudsIubf4E81xeA3h3IXA==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", "dev": true, "dependencies": { "@babel/highlight": "^7.23.4", @@ -264,9 +267,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.3.tgz", - "integrity": "sha512-BmR4bWbDIoFJmJ9z2cZ8Gmm2MXgEDgjdWgpKmKWUt54UGFJdlj31ECtbaDvCG/qVdG3AQ1SfpZEs01lUFbzLOQ==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", + "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", "dev": true, "peer": true, "engines": { @@ -274,22 +277,22 @@ } }, "node_modules/@babel/core": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.3.tgz", - "integrity": "sha512-Jg+msLuNuCJDyBvFv5+OKOUjWMZgd85bKjbICd3zWrKAo+bJ49HJufi7CQE0q0uR8NGyO6xkCACScNqyjHSZew==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.5.tgz", + "integrity": "sha512-Cwc2XjUrG4ilcfOw4wBAK+enbdgwAcAJCfGUItPBKR7Mjw4aEfAFYrLxeRp4jWgtNIKn3n2AlBOfwwafl+42/g==", "dev": true, "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.3", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.5", "@babel/helper-compilation-targets": "^7.22.15", "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.23.2", - "@babel/parser": "^7.23.3", + "@babel/helpers": "^7.23.5", + "@babel/parser": "^7.23.5", "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.3", - "@babel/types": "^7.23.3", + "@babel/traverse": "^7.23.5", + "@babel/types": "^7.23.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -315,13 +318,13 @@ } }, "node_modules/@babel/generator": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.4.tgz", - "integrity": "sha512-esuS49Cga3HcThFNebGhlgsrVLkvhqvYDTzgjfFFlHJcIfLe5jFmRRfCQ1KuBfc4Jrtn3ndLgKWAKjBE+IraYQ==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.5.tgz", + "integrity": "sha512-BPssCHrBD+0YrxviOa3QzpqwhNIXKEtOa2jQrm4FlmkC2apYgRnQcmPWiGZDlGxiNtltnUFolMe8497Esry+jA==", "dev": true, "peer": true, "dependencies": { - "@babel/types": "^7.23.4", + "@babel/types": "^7.23.5", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -483,9 +486,9 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz", - "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", "dev": true, "peer": true, "engines": { @@ -493,15 +496,15 @@ } }, "node_modules/@babel/helpers": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.4.tgz", - "integrity": "sha512-HfcMizYz10cr3h29VqyfGL6ZWIjTwWfvYBMsBVGwpcbhNGe3wQ1ZXZRPzZoAHhd9OqHadHqjQ89iVKINXnbzuw==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.5.tgz", + "integrity": "sha512-oO7us8FzTEsG3U6ag9MfdF1iA/7Z6dz+MtFhifZk8C8o453rGJFFWUP1t+ULM9TUIAzC9uxXEiXjOiVMyd7QPg==", "dev": true, "peer": true, "dependencies": { "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.4", - "@babel/types": "^7.23.4" + "@babel/traverse": "^7.23.5", + "@babel/types": "^7.23.5" }, "engines": { "node": ">=6.9.0" @@ -593,9 +596,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.4.tgz", - "integrity": "sha512-vf3Xna6UEprW+7t6EtOmFpHNAuxw3xqPZghy+brsnusscJRW5BMUzzHZc5ICjULee81WeUV2jjakG09MDglJXQ==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.5.tgz", + "integrity": "sha512-hOOqoiNXrmGdFbhgCzu6GiURxUgM27Xwd/aPuu8RfHEZPBzL1Z54okAHAQjXfcQNwvrlkAmAp4SlRTZ45vlthQ==", "peer": true, "bin": { "parser": "bin/babel-parser.js" @@ -796,9 +799,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.4.tgz", - "integrity": "sha512-2Yv65nlWnWlSpe3fXEyX5i7fx5kIKo4Qbcj+hMO0odwaneFjfXw5fdum+4yL20O0QiaHpia0cYQ9xpNMqrBwHg==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.5.tgz", + "integrity": "sha512-NdUTHcPe4C99WxPub+K9l9tK5/lV4UXIoaHSYgzco9BCyjKAAwzdBI+wWtYqHt7LJdbo74ZjRPJgzVweq1sz0w==", "dev": true, "dependencies": { "regenerator-runtime": "^0.14.0" @@ -823,20 +826,20 @@ } }, "node_modules/@babel/traverse": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.4.tgz", - "integrity": "sha512-IYM8wSUwunWTB6tFC2dkKZhxbIjHoWemdK+3f8/wq8aKhbUscxD5MX72ubd90fxvFknaLPeGw5ycU84V1obHJg==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.5.tgz", + "integrity": "sha512-czx7Xy5a6sapWWRx61m1Ke1Ra4vczu1mCTtJam5zRTBOonfdJ+S/B6HYmGYu3fJtr8GGET3si6IhgWVBhJ/m8w==", "dev": true, "peer": true, "dependencies": { - "@babel/code-frame": "^7.23.4", - "@babel/generator": "^7.23.4", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.5", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.4", - "@babel/types": "^7.23.4", + "@babel/parser": "^7.23.5", + "@babel/types": "^7.23.5", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -855,9 +858,9 @@ } }, "node_modules/@babel/types": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.4.tgz", - "integrity": "sha512-7uIFwVYpoplT5jp/kVv6EF93VaJ8H+Yn5IczYiaAi98ajzjfoZfslet/e0sLh+wVBjb2qqIut1b0S26VSafsSQ==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.5.tgz", + "integrity": "sha512-ON5kSOJwVO6xXVRTvOI0eOnWe7VdUcIpsovGo9U/Br4Ie4UVFQTboO2cYnDhAGU6Fp+UxSiT+pMft0SMHfuq6w==", "dev": true, "peer": true, "dependencies": { @@ -1253,9 +1256,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.3.tgz", - "integrity": "sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, "dependencies": { "ajv": "^6.12.4", @@ -1276,9 +1279,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.54.0.tgz", - "integrity": "sha512-ut5V+D+fOoWPgGGNj83GGjnntO39xDy6DWxO0wb7Jp3DcMX0TfIqdzHF85VTQkerdyGmuuMD9AKAo5KiNlf/AQ==", + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.55.0.tgz", + "integrity": "sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -1293,6 +1296,31 @@ "node": ">=14" } }, + "node_modules/@floating-ui/core": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.5.2.tgz", + "integrity": "sha512-Ii3MrfY/GAIN3OhXNzpCKaLxHQfJF9qvwq/kEJYdqDxeIHa01K8sldugal6TmeeXl+WMvhv9cnVzUTaFFJF09A==", + "dev": true, + "dependencies": { + "@floating-ui/utils": "^0.1.3" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.3.tgz", + "integrity": "sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==", + "dev": true, + "dependencies": { + "@floating-ui/core": "^1.4.2", + "@floating-ui/utils": "^0.1.3" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.6.tgz", + "integrity": "sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==", + "dev": true + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.13", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", @@ -1326,6 +1354,15 @@ "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", "dev": true }, + "node_modules/@internationalized/date": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.5.0.tgz", + "integrity": "sha512-nw0Q+oRkizBWMioseI8+2TeUPEyopJVz5YxoYVzR0W1v+2YytiYah7s/ot35F149q/xAg4F1gT/6eTd+tsUpFQ==", + "dev": true, + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -1810,6 +1847,39 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@melt-ui/pp": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@melt-ui/pp/-/pp-0.1.4.tgz", + "integrity": "sha512-zR+Kl3CZJPJBHW8V7YcdQCMI/dVcnW9Ct3yGbVaIywYVStVRS7F9uEDOea3xLLT2WTGodQePzPlUn53yKFu87g==", + "dev": true, + "dependencies": { + "estree-walker": "^3.0.3" + }, + "engines": { + "pnpm": ">=8.6.3" + }, + "peerDependencies": { + "@melt-ui/svelte": ">= 0.29.0", + "svelte": "^3.55.0 || ^4.0.0 || ^5.0.0-next.1" + } + }, + "node_modules/@melt-ui/svelte": { + "version": "0.61.3", + "resolved": "https://registry.npmjs.org/@melt-ui/svelte/-/svelte-0.61.3.tgz", + "integrity": "sha512-6btfQJs1zfXpZ++vftqXCYCqAq3DDdAj3TbSn/myZbFPYLib/HoFKQryg1Ctts3tIxCATaYtVQZVfSWHm8ZdWQ==", + "dev": true, + "dependencies": { + "@floating-ui/core": "^1.3.1", + "@floating-ui/dom": "^1.4.5", + "@internationalized/date": "^3.5.0", + "dequal": "^2.0.3", + "focus-trap": "^7.5.2", + "nanoid": "^4.0.2" + }, + "peerDependencies": { + "svelte": ">=3 <5" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1846,12 +1916,12 @@ } }, "node_modules/@playwright/test": { - "version": "1.40.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.40.0.tgz", - "integrity": "sha512-PdW+kn4eV99iP5gxWNSDQCbhMaDVej+RXL5xr6t04nbKLCBwYtA046t7ofoczHOm8u6c+45hpDKQVZqtqwkeQg==", + "version": "1.40.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.40.1.tgz", + "integrity": "sha512-EaaawMTOeEItCRvfmkI9v6rBkF1svM8wjl/YPRrg2N2Wmp+4qJYkWtJsbew1szfKKDm6fPLy4YAanBhIlf9dWw==", "dev": true, "dependencies": { - "playwright": "1.40.0" + "playwright": "1.40.1" }, "bin": { "playwright": "cli.js" @@ -1861,9 +1931,9 @@ } }, "node_modules/@polka/url": { - "version": "1.0.0-next.23", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.23.tgz", - "integrity": "sha512-C16M+IYz0rgRhWZdCmK+h58JMv8vijAA61gmz2rspCSwKwzBebpdcsiUmwrtJRdphuY30i6BSLEOP8ppbNLyLg==", + "version": "1.0.0-next.24", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.24.tgz", + "integrity": "sha512-2LuNTFBIO0m7kKIQvvPHN6UE63VjpmL9rnEEaOOaiSPbZK+zUOYIzBAWcED+3XYzhYsd/0mD57VdxAEqqV52CQ==", "dev": true }, "node_modules/@popperjs/core": { @@ -1875,68 +1945,82 @@ "url": "https://opencollective.com/popperjs" } }, - "node_modules/@sentry-internal/tracing": { - "version": "7.81.1", - "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.81.1.tgz", - "integrity": "sha512-E5xm27xrLXL10knH2EWDQsQYh5nb4SxxZzJ3sJwDGG9XGKzBdlp20UUhKqx00wixooVX9uCj3e4Jg8SvNB1hKg==", + "node_modules/@sentry-internal/feedback": { + "version": "7.86.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-7.86.0.tgz", + "integrity": "sha512-6rl0JYjmAKnhm4/fuFaROh4Ht8oi9f6ZeIcViCuGJcrGICZJJY0s+R77XJI78rNa82PYFrSCcnWXcGji4T8E7g==", "dependencies": { - "@sentry/core": "7.81.1", - "@sentry/types": "7.81.1", - "@sentry/utils": "7.81.1" + "@sentry/core": "7.86.0", + "@sentry/types": "7.86.0", + "@sentry/utils": "7.86.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@sentry-internal/tracing": { + "version": "7.86.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.86.0.tgz", + "integrity": "sha512-b4dUsNWlPWRwakGwR7bhOkqiFlqQszH1hhVFwrm/8s3kqEBZ+E4CeIfCvuHBHQ1cM/fx55xpXX/BU163cy+3iQ==", + "dependencies": { + "@sentry/core": "7.86.0", + "@sentry/types": "7.86.0", + "@sentry/utils": "7.86.0" }, "engines": { "node": ">=8" } }, "node_modules/@sentry/browser": { - "version": "7.81.1", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.81.1.tgz", - "integrity": "sha512-DNtS7bZEnFPKVoGazKs5wHoWC0FwsOFOOMNeDvEfouUqKKbjO7+RDHbr7H6Bo83zX4qmZWRBf8V+3n3YPIiJFw==", + "version": "7.86.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.86.0.tgz", + "integrity": "sha512-nfYWpVOmug+W7KJO7/xhA1JScMZcYHcoOVHLsUFm4znx51U4qZEk+zZDM11Q2Nw6MuDyEYg6bsH1QCwaoC6nLw==", "dependencies": { - "@sentry-internal/tracing": "7.81.1", - "@sentry/core": "7.81.1", - "@sentry/replay": "7.81.1", - "@sentry/types": "7.81.1", - "@sentry/utils": "7.81.1" + "@sentry-internal/feedback": "7.86.0", + "@sentry-internal/tracing": "7.86.0", + "@sentry/core": "7.86.0", + "@sentry/replay": "7.86.0", + "@sentry/types": "7.86.0", + "@sentry/utils": "7.86.0" }, "engines": { "node": ">=8" } }, "node_modules/@sentry/core": { - "version": "7.81.1", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.81.1.tgz", - "integrity": "sha512-tU37yAmckOGCw/moWKSwekSCWWJP15O6luIq+u7wal22hE88F3Vc5Avo8SeF3upnPR+4ejaOFH+BJTr6bgrs6Q==", + "version": "7.86.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.86.0.tgz", + "integrity": "sha512-SbLvqd1bRYzhDS42u7GMnmbDMfth/zRiLElQWbLK/shmuZzTcfQSwNNdF4Yj+VfjOkqPFgGmICHSHVUc9dh01g==", "dependencies": { - "@sentry/types": "7.81.1", - "@sentry/utils": "7.81.1" + "@sentry/types": "7.86.0", + "@sentry/utils": "7.86.0" }, "engines": { "node": ">=8" } }, "node_modules/@sentry/replay": { - "version": "7.81.1", - "resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.81.1.tgz", - "integrity": "sha512-4ueT0C4bYjngN/9p0fEYH10dTMLovHyk9HxJ6zSTgePvGVexhg+cSEHXisoBDwHeRZVnbIvsVM0NA7rmEDXJJw==", + "version": "7.86.0", + "resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.86.0.tgz", + "integrity": "sha512-YYZO8bfQSx1H87Te/zzyHPLHvExWiYwUfMWW68yGX+PPZIIzxaM81/iCQHkoucxlvuPCOtxCgf7RSMbsnqEa8g==", "dependencies": { - "@sentry-internal/tracing": "7.81.1", - "@sentry/core": "7.81.1", - "@sentry/types": "7.81.1", - "@sentry/utils": "7.81.1" + "@sentry-internal/tracing": "7.86.0", + "@sentry/core": "7.86.0", + "@sentry/types": "7.86.0", + "@sentry/utils": "7.86.0" }, "engines": { "node": ">=12" } }, "node_modules/@sentry/svelte": { - "version": "7.81.1", - "resolved": "https://registry.npmjs.org/@sentry/svelte/-/svelte-7.81.1.tgz", - "integrity": "sha512-RXh2sUOflIqp/q5ZFln9UKVCqFPfk3Bm4lZqfDEToJYc6RGa+wadCQclVgXZZAP3a2Lw/BoZpEfx6gmo/jm2jQ==", + "version": "7.86.0", + "resolved": "https://registry.npmjs.org/@sentry/svelte/-/svelte-7.86.0.tgz", + "integrity": "sha512-Qf81DORscJrr/8nXdfRRIPThjfGD+db6VvuoNCwQ+/t3WyqSVz96huURkImTBsnQUY0Bw1y4R72ySHRzg0ynNA==", "dependencies": { - "@sentry/browser": "7.81.1", - "@sentry/types": "7.81.1", - "@sentry/utils": "7.81.1", + "@sentry/browser": "7.86.0", + "@sentry/types": "7.86.0", + "@sentry/utils": "7.86.0", "magic-string": "^0.30.0" }, "engines": { @@ -1947,30 +2031,30 @@ } }, "node_modules/@sentry/tracing": { - "version": "7.81.1", - "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-7.81.1.tgz", - "integrity": "sha512-of9WMu0XgEBl9onTEk8SMaxj4BUadaUvHH96T1OpRMjdyuCM/3u2mjCkh3ekINr3Fu/uT2kJ/kO3goUxfcdXIQ==", + "version": "7.86.0", + "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-7.86.0.tgz", + "integrity": "sha512-WPqgmbLm6ntpIoTZd1L/RHIVEDMmvVjIDxKeXGiJeXHZG2VMtgwoxuZAFluVFaD0Sr20Nhj+ZS7HvKOWTxrjjA==", "dependencies": { - "@sentry-internal/tracing": "7.81.1" + "@sentry-internal/tracing": "7.86.0" }, "engines": { "node": ">=8" } }, "node_modules/@sentry/types": { - "version": "7.81.1", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.81.1.tgz", - "integrity": "sha512-dvJvGyctiaPMIQqa46k56Re5IODWMDxiHJ1UjBs/WYDLrmWFPGrEbyJ8w8CYLhYA+7qqrCyIZmHbWSTRIxstHw==", + "version": "7.86.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.86.0.tgz", + "integrity": "sha512-pGAt0+bMfWgo0KG2epthfNV4Wae03tURpoxNjGo5Fr4cXxvLTSijSAQ6rmmO4bXBJ7+rErEjX30g30o/eEdP9g==", "engines": { "node": ">=8" } }, "node_modules/@sentry/utils": { - "version": "7.81.1", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.81.1.tgz", - "integrity": "sha512-gq+MDXIirHKxNZ+c9/lVvCXd6y2zaZANujwlFggRH2u9SRiPaIXVilLpvMm4uJqmqBMEcY81ArujExtHvkbCqg==", + "version": "7.86.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.86.0.tgz", + "integrity": "sha512-6PejFtw9VTFFy5vu0ks+U7Ozkqz+eMt+HN8AZKBKErYzX5/xs0kpkOcSRpu3ETdTYcZf8VAmLVgFgE2BE+3WuQ==", "dependencies": { - "@sentry/types": "7.81.1" + "@sentry/types": "7.86.0" }, "engines": { "node": ">=8" @@ -2002,6 +2086,11 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@stripe/stripe-js": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-2.2.0.tgz", + "integrity": "sha512-YyXQbsXvnNRJ6MofFhCLIQ4W7UpfkfSOQhjIaHEiCMBv3IBxhzugXiYNNzceGTK/7DL31v7HtTnkJ+FI+6AIow==" + }, "node_modules/@sveltejs/adapter-static": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-2.0.3.tgz", @@ -2012,9 +2101,9 @@ } }, "node_modules/@sveltejs/kit": { - "version": "1.27.6", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.27.6.tgz", - "integrity": "sha512-GsjTkMbKzXdbeRg0tk8S7HNShQ4879ftRr0ZHaZfjbig1xQwG57Bvcm9U9/mpLJtCapLbLWUnygKrgcLISLC8A==", + "version": "1.27.7", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.27.7.tgz", + "integrity": "sha512-AzXYDoYt42clCBwLF9GTHsXyg2DFR31Ncyt8yxu8Aw4tgB433V+w+hcr1RTfAN9zKW2J2PY9FMQ8FoX/4Vw8CA==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -2082,6 +2171,15 @@ "vite": "^4.0.0" } }, + "node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dev": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@testing-library/dom": { "version": "9.3.3", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.3.tgz", @@ -2297,9 +2395,9 @@ } }, "node_modules/@types/jest": { - "version": "29.5.10", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.10.tgz", - "integrity": "sha512-tE4yxKEphEyxj9s4inideLHktW/x6DwesIwWZ9NN1FKf9zbJYsnhBoA9vrHA/IuIOKwPa5PcFBNV4lpMIOEzyQ==", + "version": "29.5.11", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.11.tgz", + "integrity": "sha512-S2mHmYIVe13vrm6q4kN6fLYYAka15ALQki/vgDC3mIukEOx8WJlv0kQPM+d4w8Gp6u0uSdKND04IlTXBv0rwnQ==", "dev": true, "dependencies": { "expect": "^29.0.0", @@ -2345,9 +2443,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.0.tgz", - "integrity": "sha512-D0WfRmU9TQ8I9PFx9Yc+EBHw+vSpIub4IDvQivcp26PtPrdMGAq5SDcpXEo/epqa/DXotVpekHiLNTg3iaKXBQ==", + "version": "20.10.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.4.tgz", + "integrity": "sha512-D08YG6rr8X90YB56tSIuBaddy/UXAA9RKJoFvrsnogAum/0pmjkgi4+2nx96A330FmioegBWmEYQ+syqCFaveg==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -2402,16 +2500,16 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.12.0.tgz", - "integrity": "sha512-XOpZ3IyJUIV1b15M7HVOpgQxPPF7lGXgsfcEIu3yDxFPaf/xZKt7s9QO/pbk7vpWQyVulpJbu4E5LwpZiQo4kA==", + "version": "6.13.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.13.2.tgz", + "integrity": "sha512-3+9OGAWHhk4O1LlcwLBONbdXsAhLjyCFogJY/cWy2lxdVJ2JrcTF2pTGMaLl2AE7U1l31n8Py4a8bx5DLf/0dQ==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.12.0", - "@typescript-eslint/type-utils": "6.12.0", - "@typescript-eslint/utils": "6.12.0", - "@typescript-eslint/visitor-keys": "6.12.0", + "@typescript-eslint/scope-manager": "6.13.2", + "@typescript-eslint/type-utils": "6.13.2", + "@typescript-eslint/utils": "6.13.2", + "@typescript-eslint/visitor-keys": "6.13.2", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -2437,15 +2535,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.12.0.tgz", - "integrity": "sha512-s8/jNFPKPNRmXEnNXfuo1gemBdVmpQsK1pcu+QIvuNJuhFzGrpD7WjOcvDc/+uEdfzSYpNu7U/+MmbScjoQ6vg==", + "version": "6.13.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.13.2.tgz", + "integrity": "sha512-MUkcC+7Wt/QOGeVlM8aGGJZy1XV5YKjTpq9jK6r6/iLsGXhBVaGP5N0UYvFsu9BFlSpwY9kMretzdBH01rkRXg==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "6.12.0", - "@typescript-eslint/types": "6.12.0", - "@typescript-eslint/typescript-estree": "6.12.0", - "@typescript-eslint/visitor-keys": "6.12.0", + "@typescript-eslint/scope-manager": "6.13.2", + "@typescript-eslint/types": "6.13.2", + "@typescript-eslint/typescript-estree": "6.13.2", + "@typescript-eslint/visitor-keys": "6.13.2", "debug": "^4.3.4" }, "engines": { @@ -2465,13 +2563,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.12.0.tgz", - "integrity": "sha512-5gUvjg+XdSj8pcetdL9eXJzQNTl3RD7LgUiYTl8Aabdi8hFkaGSYnaS6BLc0BGNaDH+tVzVwmKtWvu0jLgWVbw==", + "version": "6.13.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.2.tgz", + "integrity": "sha512-CXQA0xo7z6x13FeDYCgBkjWzNqzBn8RXaE3QVQVIUm74fWJLkJkaHmHdKStrxQllGh6Q4eUGyNpMe0b1hMkXFA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.12.0", - "@typescript-eslint/visitor-keys": "6.12.0" + "@typescript-eslint/types": "6.13.2", + "@typescript-eslint/visitor-keys": "6.13.2" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -2482,13 +2580,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.12.0.tgz", - "integrity": "sha512-WWmRXxhm1X8Wlquj+MhsAG4dU/Blvf1xDgGaYCzfvStP2NwPQh6KBvCDbiOEvaE0filhranjIlK/2fSTVwtBng==", + "version": "6.13.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.13.2.tgz", + "integrity": "sha512-Qr6ssS1GFongzH2qfnWKkAQmMUyZSyOr0W54nZNU1MDfo+U4Mv3XveeLZzadc/yq8iYhQZHYT+eoXJqnACM1tw==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "6.12.0", - "@typescript-eslint/utils": "6.12.0", + "@typescript-eslint/typescript-estree": "6.13.2", + "@typescript-eslint/utils": "6.13.2", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, @@ -2509,9 +2607,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.12.0.tgz", - "integrity": "sha512-MA16p/+WxM5JG/F3RTpRIcuOghWO30//VEOvzubM8zuOOBYXsP+IfjoCXXiIfy2Ta8FRh9+IO9QLlaFQUU+10Q==", + "version": "6.13.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.2.tgz", + "integrity": "sha512-7sxbQ+EMRubQc3wTfTsycgYpSujyVbI1xw+3UMRUcrhSy+pN09y/lWzeKDbvhoqcRbHdc+APLs/PWYi/cisLPg==", "dev": true, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -2522,13 +2620,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.12.0.tgz", - "integrity": "sha512-vw9E2P9+3UUWzhgjyyVczLWxZ3GuQNT7QpnIY3o5OMeLO/c8oHljGc8ZpryBMIyympiAAaKgw9e5Hl9dCWFOYw==", + "version": "6.13.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.2.tgz", + "integrity": "sha512-SuD8YLQv6WHnOEtKv8D6HZUzOub855cfPnPMKvdM/Bh1plv1f7Q/0iFUDLKKlxHcEstQnaUU4QZskgQq74t+3w==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.12.0", - "@typescript-eslint/visitor-keys": "6.12.0", + "@typescript-eslint/types": "6.13.2", + "@typescript-eslint/visitor-keys": "6.13.2", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -2549,17 +2647,17 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.12.0.tgz", - "integrity": "sha512-LywPm8h3tGEbgfyjYnu3dauZ0U7R60m+miXgKcZS8c7QALO9uWJdvNoP+duKTk2XMWc7/Q3d/QiCuLN9X6SWyQ==", + "version": "6.13.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.13.2.tgz", + "integrity": "sha512-b9Ptq4eAZUym4idijCRzl61oPCwwREcfDI8xGk751Vhzig5fFZR9CyzDz4Sp/nxSLBYxUPyh4QdIDqWykFhNmQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.12.0", - "@typescript-eslint/types": "6.12.0", - "@typescript-eslint/typescript-estree": "6.12.0", + "@typescript-eslint/scope-manager": "6.13.2", + "@typescript-eslint/types": "6.13.2", + "@typescript-eslint/typescript-estree": "6.13.2", "semver": "^7.5.4" }, "engines": { @@ -2574,12 +2672,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.12.0.tgz", - "integrity": "sha512-rg3BizTZHF1k3ipn8gfrzDXXSFKyOEB5zxYXInQ6z0hUvmQlhaZQzK+YmHmNViMA9HzW5Q9+bPPt90bU6GQwyw==", + "version": "6.13.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.2.tgz", + "integrity": "sha512-OGznFs0eAQXJsp+xSd6k/O1UbFi/K/L7WjqeRoFE7vadjAF9y0uppXhYNQNEqygjou782maGClOoZwPqF0Drlw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.12.0", + "@typescript-eslint/types": "6.13.2", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -2680,13 +2778,13 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.3.9", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.9.tgz", - "integrity": "sha512-+/Lf68Vr/nFBA6ol4xOtJrW+BQWv3QWKfRwGSm70jtXwfhZNF4R/eRgyVJYoxFRhdCTk/F6g99BP0ffPgZihfQ==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.11.tgz", + "integrity": "sha512-h97/TGWBilnLuRaj58sxNrsUU66fwdRKLOLQ9N/5iNDfp+DZhYH9Obhe0bXxhedl8fjAgpRANpiZfbgWyruQ0w==", "peer": true, "dependencies": { - "@babel/parser": "^7.23.3", - "@vue/shared": "3.3.9", + "@babel/parser": "^7.23.5", + "@vue/shared": "3.3.11", "estree-walker": "^2.0.2", "source-map-js": "^1.0.2" } @@ -2698,30 +2796,30 @@ "peer": true }, "node_modules/@vue/compiler-dom": { - "version": "3.3.9", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.9.tgz", - "integrity": "sha512-nfWubTtLXuT4iBeDSZ5J3m218MjOy42Vp2pmKVuBKo2/BLcrFUX8nCSr/bKRFiJ32R8qbdnnnBgRn9AdU5v0Sg==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.11.tgz", + "integrity": "sha512-zoAiUIqSKqAJ81WhfPXYmFGwDRuO+loqLxvXmfUdR5fOitPoUiIeFI9cTTyv9MU5O1+ZZglJVTusWzy+wfk5hw==", "peer": true, "dependencies": { - "@vue/compiler-core": "3.3.9", - "@vue/shared": "3.3.9" + "@vue/compiler-core": "3.3.11", + "@vue/shared": "3.3.11" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.3.9", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.3.9.tgz", - "integrity": "sha512-wy0CNc8z4ihoDzjASCOCsQuzW0A/HP27+0MDSSICMjVIFzk/rFViezkR3dzH+miS2NDEz8ywMdbjO5ylhOLI2A==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.3.11.tgz", + "integrity": "sha512-U4iqPlHO0KQeK1mrsxCN0vZzw43/lL8POxgpzcJweopmqtoYy9nljJzWDIQS3EfjiYhfdtdk9Gtgz7MRXnz3GA==", "peer": true, "dependencies": { - "@babel/parser": "^7.23.3", - "@vue/compiler-core": "3.3.9", - "@vue/compiler-dom": "3.3.9", - "@vue/compiler-ssr": "3.3.9", - "@vue/reactivity-transform": "3.3.9", - "@vue/shared": "3.3.9", + "@babel/parser": "^7.23.5", + "@vue/compiler-core": "3.3.11", + "@vue/compiler-dom": "3.3.11", + "@vue/compiler-ssr": "3.3.11", + "@vue/reactivity-transform": "3.3.11", + "@vue/shared": "3.3.11", "estree-walker": "^2.0.2", "magic-string": "^0.30.5", - "postcss": "^8.4.31", + "postcss": "^8.4.32", "source-map-js": "^1.0.2" } }, @@ -2732,33 +2830,33 @@ "peer": true }, "node_modules/@vue/compiler-ssr": { - "version": "3.3.9", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.9.tgz", - "integrity": "sha512-NO5oobAw78R0G4SODY5A502MGnDNiDjf6qvhn7zD7TJGc8XDeIEw4fg6JU705jZ/YhuokBKz0A5a/FL/XZU73g==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.11.tgz", + "integrity": "sha512-Zd66ZwMvndxRTgVPdo+muV4Rv9n9DwQ4SSgWWKWkPFebHQfVYRrVjeygmmDmPewsHyznCNvJ2P2d6iOOhdv8Qg==", "peer": true, "dependencies": { - "@vue/compiler-dom": "3.3.9", - "@vue/shared": "3.3.9" + "@vue/compiler-dom": "3.3.11", + "@vue/shared": "3.3.11" } }, "node_modules/@vue/reactivity": { - "version": "3.3.9", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.3.9.tgz", - "integrity": "sha512-VmpIqlNp+aYDg2X0xQhJqHx9YguOmz2UxuUJDckBdQCNkipJvfk9yA75woLWElCa0Jtyec3lAAt49GO0izsphw==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.3.11.tgz", + "integrity": "sha512-D5tcw091f0nuu+hXq5XANofD0OXnBmaRqMYl5B3fCR+mX+cXJIGNw/VNawBqkjLNWETrFW0i+xH9NvDbTPVh7g==", "peer": true, "dependencies": { - "@vue/shared": "3.3.9" + "@vue/shared": "3.3.11" } }, "node_modules/@vue/reactivity-transform": { - "version": "3.3.9", - "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.3.9.tgz", - "integrity": "sha512-HnUFm7Ry6dFa4Lp63DAxTixUp8opMtQr6RxQCpDI1vlh12rkGIeYqMvJtK+IKyEfEOa2I9oCkD1mmsPdaGpdVg==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.3.11.tgz", + "integrity": "sha512-fPGjH0wqJo68A0wQ1k158utDq/cRyZNlFoxGwNScE28aUFOKFEnCBsvyD8jHn+0kd0UKVpuGuaZEQ6r9FJRqCg==", "peer": true, "dependencies": { - "@babel/parser": "^7.23.3", - "@vue/compiler-core": "3.3.9", - "@vue/shared": "3.3.9", + "@babel/parser": "^7.23.5", + "@vue/compiler-core": "3.3.11", + "@vue/shared": "3.3.11", "estree-walker": "^2.0.2", "magic-string": "^0.30.5" } @@ -2770,49 +2868,50 @@ "peer": true }, "node_modules/@vue/runtime-core": { - "version": "3.3.9", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.3.9.tgz", - "integrity": "sha512-xxaG9KvPm3GTRuM4ZyU8Tc+pMVzcu6eeoSRQJ9IE7NmCcClW6z4B3Ij6L4EDl80sxe/arTtQ6YmgiO4UZqRc+w==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.3.11.tgz", + "integrity": "sha512-g9ztHGwEbS5RyWaOpXuyIVFTschclnwhqEbdy5AwGhYOgc7m/q3NFwr50MirZwTTzX55JY8pSkeib9BX04NIpw==", "peer": true, "dependencies": { - "@vue/reactivity": "3.3.9", - "@vue/shared": "3.3.9" + "@vue/reactivity": "3.3.11", + "@vue/shared": "3.3.11" } }, "node_modules/@vue/runtime-dom": { - "version": "3.3.9", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.3.9.tgz", - "integrity": "sha512-e7LIfcxYSWbV6BK1wQv9qJyxprC75EvSqF/kQKe6bdZEDNValzeRXEVgiX7AHI6hZ59HA4h7WT5CGvm69vzJTQ==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.3.11.tgz", + "integrity": "sha512-OlhtV1PVpbgk+I2zl+Y5rQtDNcCDs12rsRg71XwaA2/Rbllw6mBLMi57VOn8G0AjOJ4Mdb4k56V37+g8ukShpQ==", "peer": true, "dependencies": { - "@vue/runtime-core": "3.3.9", - "@vue/shared": "3.3.9", + "@vue/runtime-core": "3.3.11", + "@vue/shared": "3.3.11", "csstype": "^3.1.2" } }, "node_modules/@vue/server-renderer": { - "version": "3.3.9", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.3.9.tgz", - "integrity": "sha512-w0zT/s5l3Oa3ZjtLW88eO4uV6AQFqU8X5GOgzq7SkQQu6vVr+8tfm+OI2kDBplS/W/XgCBuFXiPw6T5EdwXP0A==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.3.11.tgz", + "integrity": "sha512-AIWk0VwwxCAm4wqtJyxBylRTXSy1wCLOKbWxHaHiu14wjsNYtiRCSgVuqEPVuDpErOlRdNnuRgipQfXRLjLN5A==", "peer": true, "dependencies": { - "@vue/compiler-ssr": "3.3.9", - "@vue/shared": "3.3.9" + "@vue/compiler-ssr": "3.3.11", + "@vue/shared": "3.3.11" }, "peerDependencies": { - "vue": "3.3.9" + "vue": "3.3.11" } }, "node_modules/@vue/shared": { - "version": "3.3.9", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.9.tgz", - "integrity": "sha512-ZE0VTIR0LmYgeyhurPTpy4KzKsuDyQbMSdM49eKkMnT5X4VfFBLysMzjIZhLEFQYjjOVVfbvUDHckwjDFiO2eA==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.11.tgz", + "integrity": "sha512-u2G8ZQ9IhMWTMXaWqZycnK4UthG1fA238CD+DP4Dm4WJi5hdUKKLg0RMRaRpDPNMdkTwIDkp7WtD0Rd9BH9fLw==", "peer": true }, "node_modules/abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", "dev": true }, "node_modules/acorn": { @@ -2836,9 +2935,9 @@ } }, "node_modules/acorn-walk": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.0.tgz", - "integrity": "sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA==", + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.1.tgz", + "integrity": "sha512-TgUZgYvqZprrl7YldZNoa9OciCAyZR+Ejm9eXzKCmjsF5IKp/wgQ7Z/ZpjpGTIUPwrHQIcYeI8qDh4PsEwxMbw==", "dev": true, "engines": { "node": ">=0.4.0" @@ -2857,9 +2956,9 @@ } }, "node_modules/ai": { - "version": "2.2.25", - "resolved": "https://registry.npmjs.org/ai/-/ai-2.2.25.tgz", - "integrity": "sha512-iXu3pzPpDPBGDir8hz86FAJmJTynMfBH4V/DXyR3GGeaHOfjdrErlcMm862CSalR6PDMwDOSCQVwJqtdnvIncQ==", + "version": "2.2.28", + "resolved": "https://registry.npmjs.org/ai/-/ai-2.2.28.tgz", + "integrity": "sha512-NUPwtXjIghlkhrR8x9nEnzpaKnWVhABe4XsSYIRbwWS1f9M8DxV413W+bpQ52iKkzputuraipHSyjnAtgU88cA==", "dependencies": { "eventsource-parser": "1.0.0", "nanoid": "3.3.6", @@ -3249,9 +3348,9 @@ } }, "node_modules/browserslist": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", - "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz", + "integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==", "dev": true, "funding": [ { @@ -3269,9 +3368,9 @@ ], "peer": true, "dependencies": { - "caniuse-lite": "^1.0.30001541", - "electron-to-chromium": "^1.4.535", - "node-releases": "^2.0.13", + "caniuse-lite": "^1.0.30001565", + "electron-to-chromium": "^1.4.601", + "node-releases": "^2.0.14", "update-browserslist-db": "^1.0.13" }, "bin": { @@ -3349,9 +3448,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001564", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001564.tgz", - "integrity": "sha512-DqAOf+rhof+6GVx1y+xzbFPeOumfQnhYzVnZD6LAXijR77yPtm9mfOcqOnT3mpnJiZVT+kwLAFnRlZcIz+c6bg==", + "version": "1.0.30001566", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001566.tgz", + "integrity": "sha512-ggIhCsTxmITBAMmK8yZjEhCO5/47jKXPu6Dha/wuCS4JePVL+3uiDEBuhu2aIoT+bqTOR8L76Ip1ARL9xYsEJA==", "dev": true, "funding": [ { @@ -3717,9 +3816,9 @@ } }, "node_modules/csstype": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", - "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "peer": true }, "node_modules/data-urls": { @@ -3964,6 +4063,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "deprecated": "Use your platform's native DOMException instead", "dev": true, "dependencies": { "webidl-conversions": "^7.0.0" @@ -4004,9 +4104,9 @@ "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" }, "node_modules/electron-to-chromium": { - "version": "1.4.594", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.594.tgz", - "integrity": "sha512-xT1HVAu5xFn7bDfkjGQi9dNpMqGchUkebwf1GL7cZN32NSwwlHRPMSDJ1KN6HkS0bWUtndbSQZqvpQftKG2uFQ==", + "version": "1.4.608", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.608.tgz", + "integrity": "sha512-J2f/3iIIm3Mo0npneITZ2UPe4B1bg8fTNrFjD8715F/k1BvbviRuqYGkET1PgprrczXYTHFvotbBOmUp6KE0uA==", "dev": true, "peer": true }, @@ -4136,15 +4236,15 @@ } }, "node_modules/eslint": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.54.0.tgz", - "integrity": "sha512-NY0DfAkM8BIZDVl6PgSa1ttZbx3xHgJzSNJKYcQglem6CppHyMhRIQkBVSSMaSRnLhig3jsDbEzOjwCVt4AmmA==", + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.55.0.tgz", + "integrity": "sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.3", - "@eslint/js": "8.54.0", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.55.0", "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -4203,9 +4303,9 @@ } }, "node_modules/eslint-config-prettier": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz", - "integrity": "sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", "dev": true, "bin": { "eslint-config-prettier": "bin/cli.js" @@ -4547,6 +4647,15 @@ "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", "dev": true }, + "node_modules/focus-trap": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.5.4.tgz", + "integrity": "sha512-N7kHdlgsO/v+iD/dMoJKtsSqs5Dz/dXZVebRgJw23LDk+jMi/974zyiOYDziY2JPp8xivq9BmUGwIJMiuSBi7w==", + "dev": true, + "dependencies": { + "tabbable": "^6.2.0" + } + }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -6833,9 +6942,9 @@ "peer": true }, "node_modules/node-releases": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", - "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", "dev": true, "peer": true }, @@ -6904,12 +7013,12 @@ } }, "node_modules/object.assign": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", - "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", "has-symbols": "^1.0.3", "object-keys": "^1.1.1" }, @@ -7229,12 +7338,12 @@ } }, "node_modules/playwright": { - "version": "1.40.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.40.0.tgz", - "integrity": "sha512-gyHAgQjiDf1m34Xpwzaqb76KgfzYrhK7iih+2IzcOCoZWr/8ZqmdBw+t0RU85ZmfJMgtgAiNtBQ/KS2325INXw==", + "version": "1.40.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.40.1.tgz", + "integrity": "sha512-2eHI7IioIpQ0bS1Ovg/HszsN/XKNwEG1kbzSDDmADpclKc7CyqkHw7Mg2JCz/bbCxg25QUPcjksoMW7JcIFQmw==", "dev": true, "dependencies": { - "playwright-core": "1.40.0" + "playwright-core": "1.40.1" }, "bin": { "playwright": "cli.js" @@ -7247,9 +7356,9 @@ } }, "node_modules/playwright-core": { - "version": "1.40.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.40.0.tgz", - "integrity": "sha512-fvKewVJpGeca8t0ipM56jkVSU6Eo0RmFvQ/MaCQNDYm+sdvKkMBBWTE1FdeMqIdumRaXXjZChWHvIzCGM/tA/Q==", + "version": "1.40.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.40.1.tgz", + "integrity": "sha512-+hkOycxPiV534c4HhpfX6yrlawqVUzITRKwHAmYfmsVreltEl6fAZJ3DPfLMOODw0H3s1Itd6MDCWmP1fl/QvQ==", "dev": true, "bin": { "playwright-core": "cli.js" @@ -7273,9 +7382,9 @@ } }, "node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "version": "8.4.32", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.32.tgz", + "integrity": "sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==", "funding": [ { "type": "opencollective", @@ -7291,7 +7400,7 @@ } ], "dependencies": { - "nanoid": "^3.3.6", + "nanoid": "^3.3.7", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" }, @@ -7410,9 +7519,9 @@ } }, "node_modules/prettier": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz", - "integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.4.tgz", + "integrity": "sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" @@ -7877,9 +7986,9 @@ "dev": true }, "node_modules/seroval": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/seroval/-/seroval-0.14.1.tgz", - "integrity": "sha512-ZlC9y1KVDhZFdEHLYZup1RjKDutyX1tt3ffOauqRbRURa2vRr2NU/bHuVEuNEqR3zE2uCU3WM6LqH6Oinc3tWg==", + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/seroval/-/seroval-0.15.1.tgz", + "integrity": "sha512-OPVtf0qmeC7RW+ScVX+7aOS+xoIM7pWcZ0jOWg2aTZigCydgRB04adfteBRbecZnnrO1WuGQ+C3tLeBBzX2zSQ==", "peer": true, "engines": { "node": ">=10" @@ -8024,13 +8133,13 @@ } }, "node_modules/solid-js": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.8.6.tgz", - "integrity": "sha512-yiH6ZfBBZ3xj/aU/PBpVKB+8r8WWp100NGF7k/Z0IrK9Y8Lv0jwvFiJY1cHdc6Tj7GqXArKnMBabM0m1k+LzkA==", + "version": "1.8.7", + "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.8.7.tgz", + "integrity": "sha512-9dzrSVieh2zj3SnJ02II6xZkonR6c+j/91b7XZUNcC6xSaldlqjjGh98F1fk5cRJ8ZTkzqF5fPIWDxEOs6QZXA==", "peer": true, "dependencies": { "csstype": "^3.1.0", - "seroval": "^0.14.1" + "seroval": "^0.15.1" } }, "node_modules/solid-swr-store": { @@ -8134,9 +8243,9 @@ "dev": true }, "node_modules/std-env": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.5.0.tgz", - "integrity": "sha512-JGUEaALvL0Mf6JCfYnJOTcobY+Nc7sG/TemDRBqCA0wEr4DER7zDchaaixTlmOxAjG1uRJmX82EQcxwTQTkqVA==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.6.0.tgz", + "integrity": "sha512-aFZ19IgVmhdB2uX599ve2kE6BIE3YMnQ6Gp6BURhW/oIzpXGKr878TQfAQZn1+i0Flcc/UKUy1gOlcfaUBCryg==", "dev": true }, "node_modules/stop-iteration-iterator": { @@ -8302,9 +8411,9 @@ } }, "node_modules/svelte": { - "version": "4.2.7", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.7.tgz", - "integrity": "sha512-UExR1KS7raTdycsUrKLtStayu4hpdV3VZQgM0akX8XbXgLBlosdE/Sf3crOgyh9xIjqSYB3UEBuUlIQKRQX2hg==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.8.tgz", + "integrity": "sha512-hU6dh1MPl8gh6klQZwK/n73GiAHiR95IkFsesLPbMeEZi36ydaXL/ZAb4g9sayT0MXzpxyZjR28yderJHxcmYA==", "dependencies": { "@ampproject/remapping": "^2.2.1", "@jridgewell/sourcemap-codec": "^1.4.15", @@ -8480,6 +8589,19 @@ "node": ">=12" } }, + "node_modules/svelte-sequential-preprocessor": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/svelte-sequential-preprocessor/-/svelte-sequential-preprocessor-2.0.1.tgz", + "integrity": "sha512-N5JqlBni6BzElxmuFrOPxOJnjsxh1cFDACLEVKs8OHBcx8ZNRO1p5SxuQex1m3qbLzAC8G99EHeWcxGkjyKjLQ==", + "dev": true, + "dependencies": { + "svelte": "^4.0.1", + "tslib": "~2.6.0" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/svelte/node_modules/aria-query": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", @@ -8529,6 +8651,12 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", + "dev": true + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -8714,9 +8842,9 @@ } }, "node_modules/typescript": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz", - "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", "devOptional": true, "bin": { "tsc": "bin/tsc", @@ -8839,9 +8967,9 @@ } }, "node_modules/vite": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz", - "integrity": "sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==", + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.1.tgz", + "integrity": "sha512-AXXFaAJ8yebyqzoNB9fu2pHoo/nWX+xZlaRwoeYUxEqBO+Zj4msE5G+BhGBll9lYEKv9Hfks52PAF2X7qDYXQA==", "dev": true, "dependencies": { "esbuild": "^0.18.10", @@ -9008,16 +9136,16 @@ } }, "node_modules/vue": { - "version": "3.3.9", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.3.9.tgz", - "integrity": "sha512-sy5sLCTR8m6tvUk1/ijri3Yqzgpdsmxgj6n6yl7GXXCXqVbmW2RCXe9atE4cEI6Iv7L89v5f35fZRRr5dChP9w==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.3.11.tgz", + "integrity": "sha512-d4oBctG92CRO1cQfVBZp6WJAs0n8AK4Xf5fNjQCBeKCvMI1efGQ5E3Alt1slFJS9fZuPcFoiAiqFvQlv1X7t/w==", "peer": true, "dependencies": { - "@vue/compiler-dom": "3.3.9", - "@vue/compiler-sfc": "3.3.9", - "@vue/runtime-dom": "3.3.9", - "@vue/server-renderer": "3.3.9", - "@vue/shared": "3.3.9" + "@vue/compiler-dom": "3.3.11", + "@vue/compiler-sfc": "3.3.11", + "@vue/runtime-dom": "3.3.11", + "@vue/server-renderer": "3.3.11", + "@vue/shared": "3.3.11" }, "peerDependencies": { "typescript": "*" diff --git a/package.json b/package.json index 94f23492d..5e54238da 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,8 @@ "sync": "svelte-kit sync", "check": "svelte-check --tsconfig ./tsconfig.json --fail-on-warnings --threshold warning", "check:watch": "svelte-check --tsconfig ./tsconfig.json --watch", - "lint": "prettier --ignore-path .gitignore --check --plugin prettier-plugin-svelte . && eslint .", - "format": "prettier --ignore-path .gitignore --write --plugin prettier-plugin-svelte .", + "lint": "prettier --check . && eslint .", + "format": "prettier --write .", "test": "vitest run", "test:ui": "vitest --ui", "test:watch": "vitest watch", @@ -19,11 +19,12 @@ }, "dependencies": { "@appwrite.io/console": "npm:matej-appwrite-console@0.3.3", - "@appwrite.io/pink": "0.1.0", - "@appwrite.io/pink-icons": "^0.1.0", + "@appwrite.io/pink": "0.2.0", + "@appwrite.io/pink-icons": "0.2.0", "@popperjs/core": "^2.11.8", "@sentry/svelte": "^7.66.0", "@sentry/tracing": "^7.66.0", + "@stripe/stripe-js": "^2.2.0", "ai": "^2.2.11", "analytics": "^0.8.9", "dayjs": "^1.11.9", @@ -40,6 +41,8 @@ "web-vitals": "^3.4.0" }, "devDependencies": { + "@melt-ui/pp": "^0.1.4", + "@melt-ui/svelte": "^0.61.2", "@playwright/test": "^1.37.1", "@sveltejs/adapter-static": "^2.0.3", "@sveltejs/kit": "^1.24.0", @@ -58,13 +61,14 @@ "eslint-plugin-svelte": "^2.33.0", "jsdom": "^22.1.0", "kleur": "^4.1.5", - "prettier": "^3.0.3", - "prettier-plugin-svelte": "^3.0.3", + "prettier": "^3.2.2", + "prettier-plugin-svelte": "^3.1.2", "sass": "^1.66.1", "svelte": "^4.2.0", "svelte-check": "^3.5.1", "svelte-jester": "^2.3.2", "svelte-preprocess": "^5.0.4", + "svelte-sequential-preprocessor": "^2.0.1", "tslib": "^2.6.2", "typescript": "^5.2.2", "vite": "^4.4.9", diff --git a/src/lib/actions/analytics.ts b/src/lib/actions/analytics.ts index 5346671a7..f33e4e32d 100644 --- a/src/lib/actions/analytics.ts +++ b/src/lib/actions/analytics.ts @@ -139,6 +139,7 @@ export function isTrackingAllowed() { } export enum Submit { + DownloadDPA = 'submit_download_dpa', Error = 'submit_error', AccountCreate = 'submit_account_create', AccountLogin = 'submit_account_login', @@ -257,6 +258,25 @@ export enum Submit { FileCreate = 'submit_file_create', FileDelete = 'submit_file_delete', FileUpdatePermissions = 'submit_file_update_permissions', + BudgetCapUpdate = 'submit_budget_cap_update', + BudgetAlertsUpdate = 'submit_budget_alert_conditions_update', + CreditRedeem = 'submit_credit_redeem', + PaymentMethodCreate = 'submit_payment_method_create', + PaymentMethodUpdate = 'submit_payment_method_update', + PaymentMethodDelete = 'submit_payment_method_delete', + BillingAddressCreate = 'submit_billing_address_create', + BillingAddressUpdate = 'submit_billing_address_update', + BillingAddressDelete = 'submit_billing_address_delete', + OrganizationPaymentUpdate = 'submit_organization_payment_update', + OrganizationPaymentDelete = 'submit_organization_payment_delete', + OrganizationBackupPaymentUpdate = 'submit_organization_backup_payment_update', + OrganizationBackupPaymentDelete = 'submit_organization_backup_payment_delete', + OrganizationBillingAddressUpdate = 'submit_organization_billing_address_update', + OrganizationBillingAddressDelete = 'submit_organization_billing_address_delete', + OrganizationUpgrade = 'submit_organization_upgrade', + OrganizationDowngrade = 'submit_organization_downgrade', + OrganizationBillingTaxIdUpdate = 'submit_organization_billing_tax_id_update', + SupportTicket = 'submit_support_ticket', InstallationCreate = 'submit_installation_create', InstallationDelete = 'submit_installation_delete', EmailChangeLocale = 'submit_email_change_locale', diff --git a/src/lib/commandCenter/panels/ai.svelte b/src/lib/commandCenter/panels/ai.svelte index 1c589e138..20425fe9a 100644 --- a/src/lib/commandCenter/panels/ai.svelte +++ b/src/lib/commandCenter/panels/ai.svelte @@ -15,7 +15,6 @@ const { input, handleSubmit, completion, isLoading, complete, error } = useCompletion({ api: endpoint + '/console/assistant', headers: { - 'content-type': 'application/json', 'x-appwrite-project': 'console' }, credentials: 'include' diff --git a/src/lib/commandCenter/panels/template.svelte b/src/lib/commandCenter/panels/template.svelte index 6e4d3d2de..31a14a33e 100644 --- a/src/lib/commandCenter/panels/template.svelte +++ b/src/lib/commandCenter/panels/template.svelte @@ -369,29 +369,31 @@ --cmd-center-border: hsl(var(--color-neutral-10)); --cmd-center-shadow: 0px 16px 32px 0px rgba(55, 59, 77, 0.04); - --kbd-bg: hsl(var(--color-neutral-30)); + --kbd-bg: hsl(var(--color-neutral-15)); + --kbd-color: hsl(var(--color-neutral-60)); --crumb-bg: hsl(var(--color-neutral-10)); --crumb-color: hsl(var(--color-neutral-100)); --result-bg: hsl(var(--color-neutral-10)); - --footer-bg: linear-gradient(180deg, #fff 49.38%, #e8e9f0 100%); + --footer-bg: linear-gradient(180deg, #fff 0%, #e8e9f0 100%); --icon-color: hsl(var(--color-neutral-50)); - --label-color: hsl(var(--color-neutral-100)); + --label-color: hsl(var(--color-neutral-60)); } :global(.theme-dark) .card { - --cmd-center-bg: hsl(var(--color-neutral-100)); - --cmd-center-border: hsl(var(--color-neutral-150)); - --cmd-center-shadow: 0px 16px 32px 0px hsl(var(--color-neutral-110)); + --cmd-center-bg: hsl(var(--color-neutral-90)); + --cmd-center-border: hsl(var(--color-neutral-80)); + --cmd-center-shadow: 0px 16px 32px 0px #14141f; - --kbd-bg: hsl(var(--color-neutral-150)); + --kbd-bg: hsl(var(--color-neutral-80)); + --kbd-color: hsl(var(--color-neutral-15)); --crumb-bg: hsl(var(--color-neutral-150)); --crumb-color: hsl(var(--color-neutral-30)); - --result-bg: hsl(var(--color-neutral-200)); + --result-bg: hsl(var(--color-neutral-85)); --footer-bg: linear-gradient( 180deg, hsl(var(--color-neutral-100)) 0%, @@ -399,7 +401,7 @@ ); --icon-color: hsl(var(--color-neutral-70)); - --label-color: hsl(var(--color-neutral-30)); + --label-color: hsl(var(--color-neutral-15)); } // Elements @@ -430,6 +432,7 @@ } :global(.kbd) { + color: var(--kbd-color); background-color: var(--kbd-bg); padding-inline: 0.25rem; } diff --git a/src/lib/components/alert.svelte b/src/lib/components/alert.svelte index 4dc90b3ff..c553d6042 100644 --- a/src/lib/components/alert.svelte +++ b/src/lib/components/alert.svelte @@ -1,6 +1,7 @@ + +{#if $organization?.markedForDeletion && !$page.url.pathname.includes('/console/account')} + + + All existing projects in the {$organization.name} organization have been paused. This organization + will be deleted once your upcoming invoice is processed successfully. + + +{/if} diff --git a/src/lib/components/billing/alerts/paymentAuthRequired.svelte b/src/lib/components/billing/alerts/paymentAuthRequired.svelte new file mode 100644 index 000000000..d8819b492 --- /dev/null +++ b/src/lib/components/billing/alerts/paymentAuthRequired.svelte @@ -0,0 +1,29 @@ + + +{#if $actionRequiredInvoices && $actionRequiredInvoices?.invoices?.length && !$page.url.pathname.includes('/console/account')} + + Please authorize your upcoming payment for {$organization.name}. Your bank requires this + security measure to proceed with payment. + + + + + +{/if} diff --git a/src/lib/components/billing/alerts/projectsAtRisk.svelte b/src/lib/components/billing/alerts/projectsAtRisk.svelte new file mode 100644 index 000000000..348fe3ebd --- /dev/null +++ b/src/lib/components/billing/alerts/projectsAtRisk.svelte @@ -0,0 +1,32 @@ + + +{#if $failedInvoice && !$page.url.pathname.includes('/console/account')} + {@const daysPassed = diffDays(new Date($failedInvoice.dueAt), new Date())} + + + {#if daysPassed > 30} + Your scheduled payment on {toLocaleDate($failedInvoice?.dueAt)} failed. To resume + write access of your organization, please update your billing details. + {:else} + Your scheduled payment on {toLocaleDate($failedInvoice?.dueAt)} failed. Access + to paid projects within this organization will be disabled if no action is taken within + 30 days. + {/if} + + + + + +{/if} diff --git a/src/lib/components/billing/alerts/tooManyFreeOrgs.svelte b/src/lib/components/billing/alerts/tooManyFreeOrgs.svelte new file mode 100644 index 000000000..cf37e4d45 --- /dev/null +++ b/src/lib/components/billing/alerts/tooManyFreeOrgs.svelte @@ -0,0 +1,13 @@ + + + + All but one organization will be automatically upgraded to a Pro plan on 31 January 2024. + You can add a payment method or transfer projects from your settings. + + + + + diff --git a/src/lib/components/billing/couponInput.svelte b/src/lib/components/billing/couponInput.svelte new file mode 100644 index 000000000..1f81a3a74 --- /dev/null +++ b/src/lib/components/billing/couponInput.svelte @@ -0,0 +1,74 @@ + + + + + + + {#if couponData?.status === 'error'} +
+ + + {couponData.code} is not a valid promo code + +
+ {:else if couponData?.status === 'active'} +
+
+ + + + {couponData.code} applied (-${couponData.credits}) + + +
+ +
+ {/if} +
diff --git a/src/lib/components/billing/index.ts b/src/lib/components/billing/index.ts new file mode 100644 index 000000000..0d7a98daa --- /dev/null +++ b/src/lib/components/billing/index.ts @@ -0,0 +1,2 @@ +export { default as PaymentBoxes } from './paymentBoxes.svelte'; +export { default as CouponInput } from './couponInput.svelte'; diff --git a/src/lib/components/billing/paymentBoxes.svelte b/src/lib/components/billing/paymentBoxes.svelte new file mode 100644 index 000000000..240ad1c0c --- /dev/null +++ b/src/lib/components/billing/paymentBoxes.svelte @@ -0,0 +1,91 @@ + + + + + + + + {element.brand} ending in {element.last4} + + + + + + Add new payment method + + + + + +
+
+
+
+
+ +
+
+
+
+ + diff --git a/src/lib/components/box.svelte b/src/lib/components/box.svelte index d77ca8eb4..9d164cd57 100644 --- a/src/lib/components/box.svelte +++ b/src/lib/components/box.svelte @@ -5,7 +5,7 @@ export { classes as class }; enum radiuses { - xsmall = '--border-radius-extra-large', + xsmall = '--border-radius-xsmall', small = '--border-radius-small', medium = '--border-radius-medium', large = '--border-radius-large' diff --git a/src/lib/components/card.svelte b/src/lib/components/card.svelte index 24c81e6e1..d5a3cbdb7 100644 --- a/src/lib/components/card.svelte +++ b/src/lib/components/card.svelte @@ -24,6 +24,8 @@ export let isButton = false; export let danger = false; export let href: string = null; + let classes = ''; + export { classes as class }; function getElement() { switch (true) { @@ -39,7 +41,7 @@ @@ -18,8 +25,12 @@ {#if total > 3 ? total < limit + offset : total % 2 !== 0} - - - + {#if isCloud && serviceId && total >= planLimit} + + {:else} + + + + {/if} {/if} diff --git a/src/lib/components/cardGrid.svelte b/src/lib/components/cardGrid.svelte index 20622e636..67e0d2ad4 100644 --- a/src/lib/components/cardGrid.svelte +++ b/src/lib/components/cardGrid.svelte @@ -10,7 +10,7 @@
-
+
diff --git a/src/lib/components/cardPlanLimit.svelte b/src/lib/components/cardPlanLimit.svelte new file mode 100644 index 000000000..966337d2f --- /dev/null +++ b/src/lib/components/cardPlanLimit.svelte @@ -0,0 +1,16 @@ + + +
+
+

Upgrade your plan to add more {service}

+ +
+
diff --git a/src/lib/components/collapsibleItem.svelte b/src/lib/components/collapsibleItem.svelte index cd44bc824..8397454dc 100644 --- a/src/lib/components/collapsibleItem.svelte +++ b/src/lib/components/collapsibleItem.svelte @@ -9,7 +9,7 @@
+ type Consent = { + key: string; + accepted: Record; + }; + export const settings = writable(false); + export const show = writable(false); + export const consent = writable( + JSON.parse(globalThis?.localStorage?.getItem('consent') ?? null) + ); + consent.subscribe((value) => { + if (browser) { + globalThis.localStorage.setItem('consent', JSON.stringify(value)); + } + }); + + + + +{#if $show} + +{/if} + + +

+ We use cookies to improve your site experience. The "strictly necessary" cookies are + required for Appwrite to function. +

+
+
+ +
+ Strictly necessary cookies +

+ These are the cookies required for Appwrite to function. +

+
+
+
+ +
+ + (optional) +

+ We include analytics cookies to understand how you use our product and design + better experiences. +

+
+
+
+ + + + +
+ + diff --git a/src/lib/components/creditCardBrandImage.svelte b/src/lib/components/creditCardBrandImage.svelte new file mode 100644 index 000000000..6bfe34c7c --- /dev/null +++ b/src/lib/components/creditCardBrandImage.svelte @@ -0,0 +1,14 @@ + + +{brand} diff --git a/src/lib/components/creditCardInfo.svelte b/src/lib/components/creditCardInfo.svelte new file mode 100644 index 000000000..b3b89d0d7 --- /dev/null +++ b/src/lib/components/creditCardInfo.svelte @@ -0,0 +1,36 @@ + + +
+
+
+ +

+ {paymentMethod?.brand} ending in {paymentMethod?.last4} +

+ +
+

+ Expires {paymentMethod?.expiryMonth}/{paymentMethod?.expiryYear} +

+ {#if paymentMethod?.name} +

+ {paymentMethod.name} +

+ {/if} +
+ + +
+ {#if paymentMethod?.expired} + + This payment method has expired + + {/if} +
diff --git a/src/lib/components/drop.svelte b/src/lib/components/drop.svelte index 31077c10d..d6c899df2 100644 --- a/src/lib/components/drop.svelte +++ b/src/lib/components/drop.svelte @@ -143,7 +143,7 @@ .drop-arrow::before { content: ''; transform: rotate(45deg); - background: hsl(var(--color-neutral-200)); + background: hsl(var(--color-neutral-85)); body.theme-light & { background: hsl(var(--color-neutral-10)); diff --git a/src/lib/components/dropListItem.svelte b/src/lib/components/dropListItem.svelte index 98d7c2676..acf02686b 100644 --- a/src/lib/components/dropListItem.svelte +++ b/src/lib/components/dropListItem.svelte @@ -4,6 +4,7 @@ export let disabled = false; export let icon: string = null; export let event: string = null; + export let loading = false; function track() { if (!event) { @@ -17,10 +18,17 @@
  • -
  • diff --git a/src/lib/components/dropListLink.svelte b/src/lib/components/dropListLink.svelte index f0b0ca024..0267a21d0 100644 --- a/src/lib/components/dropListLink.svelte +++ b/src/lib/components/dropListLink.svelte @@ -1,13 +1,27 @@
  • + class="card u-grid u-cross-center u-width-full-line dashed" + class:common-section={marginTop}>
    diff --git a/src/lib/components/fakeModal.svelte b/src/lib/components/fakeModal.svelte new file mode 100644 index 000000000..4daec9409 --- /dev/null +++ b/src/lib/components/fakeModal.svelte @@ -0,0 +1,154 @@ + + + + +{#if show} + +
    + +
    +{/if} + + diff --git a/src/lib/components/filters/content.svelte b/src/lib/components/filters/content.svelte index 6da14dcc6..098376a90 100644 --- a/src/lib/components/filters/content.svelte +++ b/src/lib/components/filters/content.svelte @@ -115,7 +115,7 @@
    -
    +
      ({ @@ -130,7 +130,7 @@ options={operatorsForColumn} placeholder="Select operator" bind:value={operatorKey} /> -
    + {#if column && operator && !operator?.hideInput}
    {#if column.type === 'integer' || column.type === 'double'} diff --git a/src/lib/components/floatingActionBar.svelte b/src/lib/components/floatingActionBar.svelte index 066852314..28b7a8e3b 100644 --- a/src/lib/components/floatingActionBar.svelte +++ b/src/lib/components/floatingActionBar.svelte @@ -26,13 +26,13 @@ } :global(.theme-dark) .floating-action-bar { - border: 1px solid hsl(var(--color-neutral-200)); - background: hsl(var(--color-neutral-300)); + border: 1px solid hsl(var(--color-neutral-85)); + background: hsl(var(--color-neutral-90)); box-shadow: 0px 6px 16px 8px #14141f; } :global(.theme-light) .floating-action-bar { - border: 1px solid hsl(var(--color-neutral-30)); + border: 1px solid hsl(var(--color-neutral-15)); background: hsl(var(--color-neutral-0)); box-shadow: 0px 6px 16px 0px rgba(55, 59, 77, 0.14); } diff --git a/src/lib/components/gridItem1.svelte b/src/lib/components/gridItem1.svelte index e5041f9da..ac1bb8ea0 100644 --- a/src/lib/components/gridItem1.svelte +++ b/src/lib/components/gridItem1.svelte @@ -7,7 +7,7 @@
    - +
    diff --git a/src/lib/components/heading.svelte b/src/lib/components/heading.svelte index f6c4d4740..440693da1 100644 --- a/src/lib/components/heading.svelte +++ b/src/lib/components/heading.svelte @@ -6,8 +6,10 @@ export let size: Size; export let trimmed = true; export let id: string = null; + let classes = ''; + export { classes as class }; - + diff --git a/src/lib/components/index.ts b/src/lib/components/index.ts index 4b12577c8..781ce567c 100644 --- a/src/lib/components/index.ts +++ b/src/lib/components/index.ts @@ -46,13 +46,20 @@ export { default as EventModal } from './eventModal.svelte'; export { default as Status } from './status.svelte'; export { default as Heading } from './heading.svelte'; export { default as Output } from './output.svelte'; +export { default as Support } from './support.svelte'; export { default as ViewSelector } from './viewSelector.svelte'; export { default as LabelCard } from './labelCard.svelte'; +export { default as RegionCard } from './regionCard.svelte'; export { default as Limit } from './limit.svelte'; export { default as PaginationWithLimit } from './paginationWithLimit.svelte'; export { default as ClickableList } from './clickableList.svelte'; export { default as ClickableListItem } from './clickableListItem.svelte'; export { default as Id } from './id.svelte'; +export { default as ProgressBar } from './progressBar.svelte'; +export { default as ProgressBarBig } from './progressBarBig.svelte'; +export { default as CreditCardInfo } from './creditCardInfo.svelte'; +export { default as CreditCardBrandImage } from './creditCardBrandImage.svelte'; +export { default as CardPlanLimit } from './cardPlanLimit.svelte'; export { default as NumericList } from './numericList.svelte'; export { default as NumericListItem } from './numericListItem.svelte'; export { default as EyebrowHeading } from './eyebrowHeading.svelte'; @@ -60,3 +67,7 @@ export { default as SvgIcon } from './svgIcon.svelte'; export { default as MigrationBox } from './migrationBox.svelte'; export { default as FloatingActionBar } from './floatingActionBar.svelte'; export { default as LoadingDots } from './loadingDots.svelte'; +export { default as FakeModal } from './fakeModal.svelte'; +export { default as RadioBoxes } from './radioBoxes.svelte'; +export { default as ModalWrapper } from './modalWrapper.svelte'; +export { default as ModalSideCol } from './modalSideCol.svelte'; diff --git a/src/lib/components/labelCard.svelte b/src/lib/components/labelCard.svelte index 5565b392e..9d30f9b0c 100644 --- a/src/lib/components/labelCard.svelte +++ b/src/lib/components/labelCard.svelte @@ -1,4 +1,6 @@ diff --git a/src/lib/components/list.svelte b/src/lib/components/list.svelte index 2b3b007b5..518497ae7 100644 --- a/src/lib/components/list.svelte +++ b/src/lib/components/list.svelte @@ -1,3 +1,4 @@ -
      + +
      diff --git a/src/lib/components/listItem.svelte b/src/lib/components/listItem.svelte index 68b959ff2..26805f943 100644 --- a/src/lib/components/listItem.svelte +++ b/src/lib/components/listItem.svelte @@ -1,16 +1,8 @@ -
    • -
      - -
      - -
      -

      - -

      - -
      +
    • +
    • diff --git a/src/lib/components/modal.svelte b/src/lib/components/modal.svelte index 6d123ef69..816ce2046 100644 --- a/src/lib/components/modal.svelte +++ b/src/lib/components/modal.svelte @@ -1,6 +1,5 @@ - - - - {#if show} - - + + +
    + + {#if $$slots.footer} + + {/if} + + diff --git a/src/lib/components/modalSideCol.svelte b/src/lib/components/modalSideCol.svelte new file mode 100644 index 000000000..81dbe2464 --- /dev/null +++ b/src/lib/components/modalSideCol.svelte @@ -0,0 +1,77 @@ + + + +
    +
    + +
    +
    + + +
    +
    +
    + + diff --git a/src/lib/components/modalWrapper.svelte b/src/lib/components/modalWrapper.svelte new file mode 100644 index 000000000..0e28bdf61 --- /dev/null +++ b/src/lib/components/modalWrapper.svelte @@ -0,0 +1,82 @@ + + + + + + {#if show} + + {/if} + diff --git a/src/lib/components/progressBar.svelte b/src/lib/components/progressBar.svelte new file mode 100644 index 000000000..406f69c02 --- /dev/null +++ b/src/lib/components/progressBar.svelte @@ -0,0 +1,38 @@ + + +
    +
    +

    + {#if icon} +

    +

    + {progress}% +

    +
    +
    +
    + {used}{unit} + {max}{unit} +
    +
    diff --git a/src/lib/components/progressBarBig.svelte b/src/lib/components/progressBarBig.svelte new file mode 100644 index 000000000..3a177f6a3 --- /dev/null +++ b/src/lib/components/progressBarBig.svelte @@ -0,0 +1,36 @@ + + +
    +
    +
    +

    + {currentValue} + {currentUnit} +

    +

    {progress}%

    +
    +

    + {maxValue} + {maxUnit} +

    +
    + {#if showBar} +
    = 75 && progress < 100} + class:is-danger={progress >= 100} + style:--graph-size={Math.max(Math.min(progress, maximum), minimum) + '%'} /> + {/if} +
    diff --git a/src/lib/components/radioBoxes.svelte b/src/lib/components/radioBoxes.svelte new file mode 100644 index 000000000..74e84a85a --- /dev/null +++ b/src/lib/components/radioBoxes.svelte @@ -0,0 +1,41 @@ + + +
    + {#if total} + {#each elements as element} + {@const value = element[variableName]?.toString()} +
    + + + +
    + {/each} + {/if} + +
    + {#if total} + + + Add new {name} + + + {/if} + {#if group === null} + + {/if} +
    +
    diff --git a/src/lib/components/regionCard.svelte b/src/lib/components/regionCard.svelte new file mode 100644 index 000000000..3e1964802 --- /dev/null +++ b/src/lib/components/regionCard.svelte @@ -0,0 +1,34 @@ + + + diff --git a/src/lib/components/step.svelte b/src/lib/components/step.svelte index 17597f4c1..a229f65ba 100644 --- a/src/lib/components/step.svelte +++ b/src/lib/components/step.svelte @@ -3,12 +3,14 @@ export let current = false; export let currentSub = 0; export let isSub = false; - export let step: { text: string; substeps?: { text: string }[] }; + export let step: { text: string; disabled?: boolean; substeps?: { text: string }[] };
  • + {:else} + + {/if} + +{/if} +
    +
    +

    Troubleshooting

    + +
    + {#key $app.themeInUse} + + {/key} +
    +
    + +
    +
    +
    +
    +

    Community support

    +

    Get help from our community

    +
    +
      +
    • + +
    • +
    • + +
    • +
    +
    diff --git a/src/lib/constants.ts b/src/lib/constants.ts index a2107d03c..393623360 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -3,7 +3,12 @@ export const CARD_LIMIT = 6; // default card limit export const INTERVAL = 5 * 60000; // default interval to check for feedback export enum Dependencies { + CREDIT = 'dependency:credit', + INVOICES = 'dependency:invoices', + ADDRESS = 'dependency:address', + PAYMENT_METHODS = 'dependency:paymentMethods', ORGANIZATION = 'dependency:organization', + MEMBERS = 'dependency:members', PROJECT = 'dependency:project', PROJECT_VARIABLES = 'dependency:project_variables', PROJECT_INSTALLATIONS = 'dependency:project_installations', @@ -273,3 +278,348 @@ export const eventServices: Array = [ ] } ]; + +export const usageRates = { + 'tier-0': [ + { + id: 'members', + resource: 'Organization members', + amount: 1, + unit: '', + rate: '$20/member' + }, + { id: 'bandwith', resource: 'Bandwidth', amount: 10, unit: 'GB', rate: '$0.04/GB' }, + { id: 'storage', resource: 'Storage', amount: 2, unit: 'GB', rate: '$0.025/GB' }, + { + id: 'executions', + resource: 'Function executions', + amount: 750000, + unit: 'executions', + rate: '$2/1M executions' + }, + { + id: 'users', + resource: 'Active users', + amount: 200000, + unit: 'AU', + rate: '$0.0012/user' + }, + { + id: 'connections', + resource: 'Concurrent connections', + amount: 750, + unit: 'connections', + rate: '$5/1K connections' + } + ], + 'tier-1': [ + { + id: 'members', + resource: 'Organization members', + amount: 'Unlimited', + unit: '', + rate: '$20/member' + }, + { id: 'bandwith', resource: 'Bandwidth', amount: 1, unit: 'TB', rate: '$0.04/GB' }, + { id: 'storage', resource: 'Storage', amount: 150, unit: 'GB', rate: '$0.025/GB' }, + { + id: 'executions', + resource: 'Function executions', + amount: 3500000, + unit: 'executions', + rate: '$2/1M executions' + }, + { + id: 'users', + resource: 'Active users', + amount: 200000, + unit: 'AU', + rate: '$0.0012/user' + }, + { + id: 'connections', + resource: 'Concurrent connections', + amount: 750, + unit: 'connections', + rate: '$5/1K connections' + } + ], + 'tier-2': [ + { + id: 'members', + resource: 'Organization members', + amount: 'Unlimited', + unit: '', + rate: '$20/member' + }, + { id: 'bandwith', resource: 'Bandwidth', amount: 1, unit: 'TB', rate: '$0.04/GB' }, + { id: 'storage', resource: 'Storage', amount: 150, unit: 'GB', rate: '$0.025/GB' }, + { + id: 'executions', + resource: 'Function executions', + amount: 3500000, + unit: 'executions', + rate: '$2/1M executions' + }, + { + id: 'users', + resource: 'Active users', + amount: 200000, + unit: 'AU', + rate: '$0.0012/user' + }, + { + id: 'connections', + resource: 'Concurrent connections', + amount: 750, + unit: 'connections', + rate: '$5/1K connections' + } + ] +}; + +//resources: bandwidth, buckets, file size limit, functions, executions, users,teams,logs, members, platforms, webhooks, databases, connections, messages +export const limitRates = { + 'tier-0': [ + { + id: 'bandwith', + amount: 10, + unit: 'GB' + }, + { + id: 'buckets', + amount: 3, + unit: '' + }, + { + id: 'file-size-limit', + amount: 1, + unit: 'MB' + }, + { + id: 'storage', + amount: 5, + unit: 'GB' + }, + { + id: 'functions', + amount: 1, + unit: '' + }, + { + id: 'executions', + amount: 750000, + unit: 'executions' + }, + { + id: 'users', + amount: 200000, + unit: 'AU' + }, + + { + id: 'teams', + amount: 1, + unit: '' + }, + + { + id: 'logs', + amount: 1, + unit: '' + }, + + { + id: 'members', + amount: 1, + unit: '' + }, + { + id: 'platforms', + amount: 1, + unit: '' + }, + { + id: 'webhooks', + amount: 1, + unit: '' + }, + { + id: 'databases', + amount: 1, + unit: '' + }, + { + id: 'connections', + amount: 1, + unit: '' + }, + { + id: 'messages', + amount: 1, + unit: '' + } + ], + 'tier-1': [ + { + id: 'bandwith', + amount: 10, + unit: 'GB' + }, + { + id: 'buckets', + amount: 1, + unit: '' + }, + { + id: 'file-size-limit', + amount: 5, + unit: 'MB' + }, + { + id: 'storage', + amount: 5, + unit: 'GB' + }, + { + id: 'functions', + amount: 1, + unit: '' + }, + { + id: 'executions', + amount: 750000, + unit: 'executions' + }, + { + id: 'users', + amount: 200000, + unit: 'AU' + }, + + { + id: 'teams', + amount: 1, + unit: '' + }, + { + id: 'logs', + amount: 1, + unit: '' + }, + { + id: 'members', + amount: 1, + unit: '' + }, + { + id: 'platforms', + amount: 1, + unit: '' + }, + { + id: 'webhooks', + amount: 1, + unit: '' + }, + { + id: 'databases', + amount: 1, + unit: '' + }, + { + id: 'connections', + amount: 1, + unit: '' + }, + { + id: 'messages', + amount: 1, + unit: '' + } + ], + 'tier-2': [ + { + id: 'bandwith', + amount: 10, + unit: 'GB' + }, + { + id: 'buckets', + amount: 1, + unit: '' + }, + { + id: 'file-size-limit', + amount: 5, + unit: 'MB' + }, + { + id: 'storage', + amount: 5, + unit: 'GB' + }, + { + id: 'functions', + amount: 1, + unit: '' + }, + { + id: 'executions', + amount: 750000, + unit: 'executions' + }, + { + id: 'users', + amount: 200000, + unit: 'AU' + }, + + { + id: 'teams', + amount: 1, + unit: '' + }, + { + id: 'logs', + amount: 1, + unit: '' + }, + { + id: 'members', + amount: 1, + unit: '' + }, + { + id: 'platforms', + amount: 1, + unit: '' + }, + { + id: 'webhooks', + amount: 1, + unit: '' + }, + { + id: 'databases', + amount: 1, + unit: '' + }, + { + id: 'connections', + amount: 1, + unit: '' + }, + { + id: 'messages', + amount: 1, + unit: '' + } + ] +}; + +export enum BillingPlan { + STARTER = 'tier-0', + PRO = 'tier-1', + SCALE = 'tier-2' +} diff --git a/src/lib/elements/flag.svelte b/src/lib/elements/flag.svelte new file mode 100644 index 000000000..d1088bddb --- /dev/null +++ b/src/lib/elements/flag.svelte @@ -0,0 +1,28 @@ + + +{name} diff --git a/src/lib/elements/forms/button.svelte b/src/lib/elements/forms/button.svelte index d184b5739..f7995b3cc 100644 --- a/src/lib/elements/forms/button.svelte +++ b/src/lib/elements/forms/button.svelte @@ -11,17 +11,20 @@ export let supabase = false; export let text = false; export let danger = false; - export let disabled = false; export let round = false; + export let link = false; + export let disabled = false; export let external = false; export let href: string = null; export let fullWidth = false; + export let fullWidthMobile = false; export let ariaLabel: string = null; export let noMargin = false; export let event: string = null; let classes: string = undefined; export { classes as class }; export let actions: MultiActionArray = []; + export let submissionLoader = false; const isSubmitting = hasContext('form') ? getContext('form').isSubmitting @@ -40,7 +43,7 @@ } $: resolvedClasses = [ - 'button', + link ? 'link' : 'button', disabled && 'is-disabled', round && 'is-only-icon', secondary && 'is-secondary', @@ -49,6 +52,7 @@ text && 'is-text', danger && 'is-danger', fullWidth && 'is-full-width', + fullWidthMobile && 'is-full-width-mobile', noMargin && 'u-padding-inline-0', classes ] @@ -58,6 +62,7 @@ {#if href} - + {#if $isSubmitting && submissionLoader} +
    diff --git a/src/lib/elements/forms/index.ts b/src/lib/elements/forms/index.ts index 1f729ba4b..22f3688a2 100644 --- a/src/lib/elements/forms/index.ts +++ b/src/lib/elements/forms/index.ts @@ -27,3 +27,5 @@ export { default as InputSecret } from './inputSecret.svelte'; export { default as Helper } from './helper.svelte'; export { default as Label } from './label.svelte'; export { default as InputProjectId } from './inputProjectId.svelte'; +export { default as InputDate } from './inputDate.svelte'; +export { default as InputDateRange } from './inputDateRange.svelte'; diff --git a/src/lib/elements/forms/inputCheckbox.svelte b/src/lib/elements/forms/inputCheckbox.svelte index d32c95bd6..4aaf52c6b 100644 --- a/src/lib/elements/forms/inputCheckbox.svelte +++ b/src/lib/elements/forms/inputCheckbox.svelte @@ -1,5 +1,6 @@ - + {#if label}
    +
    {#if (label && showLabel) || tooltip}
    {#if label} diff --git a/src/lib/elements/forms/inputDate.svelte b/src/lib/elements/forms/inputDate.svelte new file mode 100644 index 000000000..a49aa3c4e --- /dev/null +++ b/src/lib/elements/forms/inputDate.svelte @@ -0,0 +1,88 @@ + + + + + +
    + + {#if isNullable} +
      +
    • + +
    • +
    + {/if} +
    + {#if error} + {error} + {/if} +
    diff --git a/src/lib/elements/forms/inputDateRange.svelte b/src/lib/elements/forms/inputDateRange.svelte new file mode 100644 index 000000000..495818d67 --- /dev/null +++ b/src/lib/elements/forms/inputDateRange.svelte @@ -0,0 +1,175 @@ + + +{#if label} + +{/if} +
    + {#if showDates} + {#each $segmentContents.start as seg} +
    + {seg.value} +
    + {/each} + + {#each $segmentContents.end as seg} +
    + {seg.value} +
    + {/each} + {/if} +
    + +
    +
    +
    +
    +
    + +
    + {$headingValue} +
    + +
    + {#each $months as month} + + + + {#each $daysOfWeek as day} + + {/each} + + + + {#each month.weeks as days} + + {#each days as date} + + {/each} + + {/each} + +
    +
    + {date.day} +
    +
    + {/each} +
    +
    + + diff --git a/src/lib/elements/forms/inputDateTime.svelte b/src/lib/elements/forms/inputDateTime.svelte index 6548e2f5e..5eb8da64d 100644 --- a/src/lib/elements/forms/inputDateTime.svelte +++ b/src/lib/elements/forms/inputDateTime.svelte @@ -7,7 +7,7 @@ export let showLabel = true; export let optionalText: string | undefined = undefined; export let id: string; - export let value = ''; + export let value: string; export let required = false; export let nullable = false; export let disabled = false; @@ -35,6 +35,11 @@ error = element.validationMessage; } + function handleInput(event: Event) { + const { value: currentValue } = event.currentTarget as HTMLInputElement; + value = currentValue || null; + } + let prevValue = ''; function handleNullChange(e: CustomEvent) { const isNull = e.detail; @@ -64,12 +69,13 @@ {disabled} {readonly} {required} + {value} step=".001" autocomplete={autocomplete ? 'on' : 'off'} type="datetime-local" class="input-text" - bind:value bind:this={element} + on:input={handleInput} on:invalid={handleInvalid} style:--amount-of-buttons={isNullable ? 2.75 : 1} style:--button-size={isNullable ? '2rem' : '1rem'} /> diff --git a/src/lib/elements/forms/inputEmail.svelte b/src/lib/elements/forms/inputEmail.svelte index cfb27a139..4b7d16731 100644 --- a/src/lib/elements/forms/inputEmail.svelte +++ b/src/lib/elements/forms/inputEmail.svelte @@ -60,7 +60,11 @@ {label} -
    +
    {/if} +
    {#if error} {error} diff --git a/src/lib/elements/forms/inputId.svelte b/src/lib/elements/forms/inputId.svelte index ea1dc166b..6100210b4 100644 --- a/src/lib/elements/forms/inputId.svelte +++ b/src/lib/elements/forms/inputId.svelte @@ -8,7 +8,7 @@ let element: HTMLInputElement; let icon = 'info'; - const pattern = String.raw`^[a-zA-Z0-9][a-zA-Z0-9._-]*$`; + const pattern = String.raw`^[a-zA-Z0-9][a-zA-Z0-9._\-]*$`; onMount(() => { if (element && autofocus) { diff --git a/src/lib/elements/forms/inputNumber.svelte b/src/lib/elements/forms/inputNumber.svelte index 7baf0ecb9..75237ecc4 100644 --- a/src/lib/elements/forms/inputNumber.svelte +++ b/src/lib/elements/forms/inputNumber.svelte @@ -1,6 +1,6 @@ - + {#if label} + diff --git a/src/lib/elements/forms/inputRadio.svelte b/src/lib/elements/forms/inputRadio.svelte index 97f1b14b4..cb3db08ce 100644 --- a/src/lib/elements/forms/inputRadio.svelte +++ b/src/lib/elements/forms/inputRadio.svelte @@ -1,7 +1,7 @@ - + {#if label} + diff --git a/src/lib/elements/forms/inputSelectSearch.svelte b/src/lib/elements/forms/inputSelectSearch.svelte index 929b528e4..6d1fad7df 100644 --- a/src/lib/elements/forms/inputSelectSearch.svelte +++ b/src/lib/elements/forms/inputSelectSearch.svelte @@ -119,7 +119,6 @@ scrollable placement="bottom-end" position="static" - fullWidth={true} fixed> {/if} -
    +
    {/if} +
    {#if error} {error} diff --git a/src/lib/elements/index.ts b/src/lib/elements/index.ts index ffd1477bb..dabf2f27b 100644 --- a/src/lib/elements/index.ts +++ b/src/lib/elements/index.ts @@ -1,2 +1,3 @@ export { default as Pill } from './pill.svelte'; export { default as SelectSearchItem } from './selectSearchItem.svelte'; +export { default as Flag } from './flag.svelte'; diff --git a/src/lib/elements/pill.svelte b/src/lib/elements/pill.svelte index 33b2c7fad..1c2ef4e6f 100644 --- a/src/lib/elements/pill.svelte +++ b/src/lib/elements/pill.svelte @@ -1,4 +1,6 @@ {#if href} @@ -28,6 +41,7 @@ {:else if button} + + + + +{/if} diff --git a/src/lib/elements/table/cell.svelte b/src/lib/elements/table/cell.svelte index b594d69bb..ba3f04ea5 100644 --- a/src/lib/elements/table/cell.svelte +++ b/src/lib/elements/table/cell.svelte @@ -5,13 +5,18 @@ export let showOverflow = false; let className = ''; export { className as class }; + export let style = ''; + export let right = false;
    diff --git a/src/lib/elements/table/cellCheck.svelte b/src/lib/elements/table/cellCheck.svelte index 9d3923d50..982c4e4eb 100644 --- a/src/lib/elements/table/cellCheck.svelte +++ b/src/lib/elements/table/cellCheck.svelte @@ -2,6 +2,7 @@ import { toggle } from '$lib/helpers/array'; import { isHTMLInputElement } from '$lib/helpers/types'; import { TableCell } from '.'; + import { InputCheckbox } from '../forms'; export let id: string; export let selectedIds: string[] = []; @@ -29,10 +30,10 @@ tabindex="-1" on:click={handleClick} on:keypress={handleClick} /> - diff --git a/src/lib/elements/table/cellHead.svelte b/src/lib/elements/table/cellHead.svelte index 3cef38388..d8bb55ca4 100644 --- a/src/lib/elements/table/cellHead.svelte +++ b/src/lib/elements/table/cellHead.svelte @@ -2,11 +2,13 @@ export let width: number = null; export let onlyDesktop = false; export let eyebrow = true; + export let style = '';
    diff --git a/src/lib/elements/table/cellHeadCheck.svelte b/src/lib/elements/table/cellHeadCheck.svelte index 6b634e982..d8da40a74 100644 --- a/src/lib/elements/table/cellHeadCheck.svelte +++ b/src/lib/elements/table/cellHeadCheck.svelte @@ -1,6 +1,7 @@ - diff --git a/src/lib/elements/table/table.svelte b/src/lib/elements/table/table.svelte index 342071287..81df3e41a 100644 --- a/src/lib/elements/table/table.svelte +++ b/src/lib/elements/table/table.svelte @@ -2,6 +2,7 @@ export let noMargin = false; export let noStyles = false; export let style = ''; + export let transparent = false;
    diff --git a/src/lib/elements/table/tableScroll.svelte b/src/lib/elements/table/tableScroll.svelte index 3e6c9f692..b586b58f5 100644 --- a/src/lib/elements/table/tableScroll.svelte +++ b/src/lib/elements/table/tableScroll.svelte @@ -3,6 +3,8 @@ export let isSticky = false; export let noMargin = false; + export let style = ''; + export let transparent = false; let isOverflowing = false; const hasOverflow: Action = (node) => { @@ -40,7 +42,11 @@
    (isOverflowing = v)}> - +
    diff --git a/src/lib/helpers/date.ts b/src/lib/helpers/date.ts index 04133478c..c8aaf0fc8 100644 --- a/src/lib/helpers/date.ts +++ b/src/lib/helpers/date.ts @@ -61,3 +61,19 @@ export const diffDays = (date1: Date, date2: Date) => { export function timeFromNow(datetime: string): string { return dayjs().to(dayjs(datetime)); } + +export function hoursToDays(hours: number) { + if (hours > 24) { + return `${Math.floor(hours / 24)} days`; + } else { + return `${hours} hour`; + } +} + +export function getTomorrow(date: Date) { + const tomorrow = new Date(date); + tomorrow.setDate(tomorrow.getDate() + 2); + tomorrow.setHours(0, 0, 0, 0); + + return tomorrow; +} diff --git a/src/lib/helpers/numbers.ts b/src/lib/helpers/numbers.ts new file mode 100644 index 000000000..796160cc2 --- /dev/null +++ b/src/lib/helpers/numbers.ts @@ -0,0 +1,22 @@ +export function abbreviateNumber(num: number, decimals: number = 1): string { + if (isNaN(num)) return String(num); + if (num >= 1000000) { + const result = num / 1000000; + return result.toFixed(result % 1 !== 0 ? decimals : 0) + 'M'; + } else if (num >= 1000) { + const result = num / 1000; + return result.toFixed(result % 1 !== 0 ? decimals : 0) + 'K'; + } else { + return num.toString(); + } +} + +export function toDecimals(num: number, decimals: number = 1): number { + return parseFloat(num.toFixed(decimals)); +} + +export function formatNumberWithCommas(number: number): string { + if (isNaN(number)) return String(number); + const formatter = new Intl.NumberFormat('en'); + return formatter.format(number); +} diff --git a/src/lib/helpers/project.ts b/src/lib/helpers/project.ts index a8a88cd3f..91cfc90ac 100644 --- a/src/lib/helpers/project.ts +++ b/src/lib/helpers/project.ts @@ -1,5 +1,6 @@ export function getProjectId() { const pathname = window.location.pathname + '/'; const projectMatch = pathname.match(/\/project-(.*?)\//); + return projectMatch?.[1] || null; } diff --git a/src/lib/helpers/sizeConvertion.ts b/src/lib/helpers/sizeConvertion.ts index 04d1c1329..41b261069 100644 --- a/src/lib/helpers/sizeConvertion.ts +++ b/src/lib/helpers/sizeConvertion.ts @@ -14,9 +14,9 @@ export function calculateSize(bytes: number, decimals = 1) { return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; } -export function sizeToBytes(value: number, unit: Size) { +export function sizeToBytes(value: number, unit: Size, base = 1024) { const index = sizes.indexOf(unit); - return value * Math.pow(1024, index); + return value * Math.pow(base, index); } export function bytesToSize(value: number, unit: Size) { @@ -28,12 +28,13 @@ export function humanFileSize(bytes: number): { value: string; unit: Size; } { + if (typeof bytes !== 'number') return { value: '0', unit: 'Bytes' }; const value = prettyBytes(bytes, { locale: 'en' }).split(' '); return { value: value[0], - unit: value[1] as Size + unit: value[1].toUpperCase() as Size }; } diff --git a/src/lib/helpers/unit.ts b/src/lib/helpers/unit.ts index 80b7e4882..394782ea2 100644 --- a/src/lib/helpers/unit.ts +++ b/src/lib/helpers/unit.ts @@ -44,11 +44,18 @@ export function createValueUnitPair(initialValue = 0, units: Unit unit.subscribe((newValue, prev) => { value.update((v) => { - const unitInBase = v * units.find((u) => u.name === prev).value; - return unitInBase / units.find((u) => u.name === newValue).value; + const prevUnit = units.find((u) => u.name === prev); + const newUnit = units.find((u) => u.name === newValue); + + // If either unit is not found, just return the current value + if (!prevUnit || !newUnit) { + return v; + } + + const unitInBase = v * prevUnit.value; + return unitInBase / newUnit.value; }); }); - return { value, unit, diff --git a/src/lib/images/login/cloud-pro-bg-dark.png b/src/lib/images/login/cloud-pro-bg-dark.png new file mode 100644 index 000000000..849c2a6cd Binary files /dev/null and b/src/lib/images/login/cloud-pro-bg-dark.png differ diff --git a/src/lib/images/login/cloud-pro-bg-light.png b/src/lib/images/login/cloud-pro-bg-light.png new file mode 100644 index 000000000..c6eb64a8e Binary files /dev/null and b/src/lib/images/login/cloud-pro-bg-light.png differ diff --git a/src/lib/images/support/support-offline-dark.png b/src/lib/images/support/support-offline-dark.png new file mode 100644 index 000000000..7a439f912 Binary files /dev/null and b/src/lib/images/support/support-offline-dark.png differ diff --git a/src/lib/images/support/support-offline-light.png b/src/lib/images/support/support-offline-light.png new file mode 100644 index 000000000..6e5b24308 Binary files /dev/null and b/src/lib/images/support/support-offline-light.png differ diff --git a/src/lib/images/support/support-online-dark.png b/src/lib/images/support/support-online-dark.png new file mode 100644 index 000000000..4d9acf779 Binary files /dev/null and b/src/lib/images/support/support-online-dark.png differ diff --git a/src/lib/images/support/support-online-light.png b/src/lib/images/support/support-online-light.png new file mode 100644 index 000000000..27c86ff6d Binary files /dev/null and b/src/lib/images/support/support-online-light.png differ diff --git a/src/lib/images/support/support-wizard-dark.svg b/src/lib/images/support/support-wizard-dark.svg new file mode 100644 index 000000000..18d44b05e --- /dev/null +++ b/src/lib/images/support/support-wizard-dark.svg @@ -0,0 +1,167 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/lib/images/support/support-wizard-light.svg b/src/lib/images/support/support-wizard-light.svg new file mode 100644 index 000000000..bf30e2f52 --- /dev/null +++ b/src/lib/images/support/support-wizard-light.svg @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/lib/layout/activity.svelte b/src/lib/layout/activity.svelte index d467beca4..e888b3b2e 100644 --- a/src/lib/layout/activity.svelte +++ b/src/lib/layout/activity.svelte @@ -15,17 +15,28 @@ TableCellText, TableScroll } from '$lib/elements/table'; - import { Container } from '$lib/layout'; - import { toLocaleDateTime } from '$lib/helpers/date'; + import { Container, ContainerHeader } from '$lib/layout'; + import { hoursToDays, toLocaleDateTime } from '$lib/helpers/date'; import type { Models } from '@appwrite.io/console'; + import { tierToPlan, type PlanServices } from '$lib/stores/billing'; + import { isCloud } from '$lib/system'; + import { organization } from '$lib/stores/organization'; + import { Button } from '$lib/elements/forms'; + import { BillingPlan } from '$lib/constants'; export let logs: Models.LogList; export let offset = 0; export let limit = 0; + + export let service: PlanServices = null; - Activity + {#if service} + + {:else} + Activity + {/if} {#if logs.total} @@ -35,7 +46,18 @@ IP Date - + + +

    + Logs are retained in rolling {hoursToDays(limit)} intervals with the + {tierToPlan($organization.billingPlan).name} + plan. + {#if $organization?.billingPlan === BillingPlan.STARTER} + to increase your log + retention for a longer period. + {/if} +

    +
    {#each logs.logs as log} diff --git a/src/lib/layout/containerButton.svelte b/src/lib/layout/containerButton.svelte new file mode 100644 index 000000000..6c39746ad --- /dev/null +++ b/src/lib/layout/containerButton.svelte @@ -0,0 +1,40 @@ + + +
    + +
    diff --git a/src/lib/layout/containerHeader.svelte b/src/lib/layout/containerHeader.svelte new file mode 100644 index 000000000..e2d60b2be --- /dev/null +++ b/src/lib/layout/containerHeader.svelte @@ -0,0 +1,165 @@ + + + +{#if isCloud && showAlert} + {#if $readOnly} + {@const services = overflowingServices + .map((s) => { + return s.name.toLocaleLowerCase(); + }) + .join(', ')} + + {#if $organization?.billingPlan !== BillingPlan.STARTER && hasUsageFees} + + + You've reached the {services} limit for the {tier} plan. + . + + + {:else} + + + You've reached the {services} limit for the {tier} plan. your organization for additional + resources. + + + {/if} + + {/if} +{/if} + +
    +
    + {title} + {#if isCloud && isLimited} + + {#if hasProjectLimitation} + (showDropdown = !showDropdown)}> + {total}/{limit} + {title} used + + {:else} + (showDropdown = !showDropdown)}> + {title} limited + + {/if} + + + {#if hasProjectLimitation} +

    + Your are limited to {limit} + {title.toLocaleLowerCase()} per project on the {tier} plan. + {#if $organization?.billingPlan === BillingPlan.STARTER} + for addtional {title.toLocaleLowerCase()}. + {/if} +

    + {:else if hasUsageFees} +

    + You are limited to {limit} + {title.toLocaleLowerCase()} per organization on the {tier} plan. + . +

    + {:else} +

    + You are limited to {limit} + {title.toLocaleLowerCase()} per organization on the {tier} plan. + {#if $organization?.billingPlan === BillingPlan.STARTER} + + for additional {title.toLocaleLowerCase()}. + {/if} +

    + {/if} +
    +
    +
    + {/if} +
    + + + {#if buttonText} + + {/if} + +
    diff --git a/src/lib/layout/footer.svelte b/src/lib/layout/footer.svelte index c1b979a80..da4b205aa 100644 --- a/src/lib/layout/footer.svelte +++ b/src/lib/layout/footer.svelte @@ -1,4 +1,6 @@ + +
    +
    +
    +
    diff --git a/src/lib/layout/index.ts b/src/lib/layout/index.ts index 2172f7e0a..cf1bd6e4b 100644 --- a/src/lib/layout/index.ts +++ b/src/lib/layout/index.ts @@ -16,3 +16,6 @@ export { default as Usage, type UsagePeriods } from './usage.svelte'; export { default as Activity } from './activity.svelte'; export { default as Progress } from './progress.svelte'; export { default as GridHeader } from './gridHeader.svelte'; +export { default as ContainerHeader } from './containerHeader.svelte'; +export { default as HeaderAlert } from './headerAlert.svelte'; +export { default as ContainerButton } from './containerButton.svelte'; diff --git a/src/lib/layout/logs.svelte b/src/lib/layout/logs.svelte index 5e126f846..cb37e7d8b 100644 --- a/src/lib/layout/logs.svelte +++ b/src/lib/layout/logs.svelte @@ -1,7 +1,7 @@ @@ -169,9 +179,24 @@
    {#if buttons}
    diff --git a/src/lib/layout/notifications.svelte b/src/lib/layout/notifications.svelte index c789018bc..e2558ddf7 100644 --- a/src/lib/layout/notifications.svelte +++ b/src/lib/layout/notifications.svelte @@ -14,9 +14,9 @@ title={notification.title} icon={notification.icon} on:dismiss={() => dismissNotification(notification.id)} - buttons={notification?.buttons}> - {notification.message} - + buttons={notification?.buttons} + message={notification.message} + html={notification?.isHtml} />
  • {/each} diff --git a/src/lib/layout/shell.svelte b/src/lib/layout/shell.svelte index b7d1ff097..318ef0000 100644 --- a/src/lib/layout/shell.svelte +++ b/src/lib/layout/shell.svelte @@ -46,7 +46,11 @@
    + class:u-hide={$wizard.show || $log.show || $wizard.cover} + class:is-fixed-layout={$$slots.alert}> + {#if $$slots.alert} + + {/if}
    {#if $wizard.media} @@ -143,10 +185,7 @@ {#if $wizard.step === 1} {:else} - + {/if}
    - - +
    diff --git a/src/lib/layout/wizardExitModal.svelte b/src/lib/layout/wizardExitModal.svelte index c3221c268..c73d2cd2e 100644 --- a/src/lib/layout/wizardExitModal.svelte +++ b/src/lib/layout/wizardExitModal.svelte @@ -8,10 +8,17 @@ function handleSubmit() { dispatch('exit'); + show = false; } - +

    Are you sure you want to exit from ? All data will be deleted. This action is irreversible. diff --git a/src/lib/layout/wizardStep.svelte b/src/lib/layout/wizardStep.svelte index 4ed953608..cddb57463 100644 --- a/src/lib/layout/wizardStep.svelte +++ b/src/lib/layout/wizardStep.svelte @@ -20,7 +20,7 @@ } -

    +
    @@ -32,3 +32,10 @@
    + + diff --git a/src/lib/sdk/billing.ts b/src/lib/sdk/billing.ts new file mode 100644 index 000000000..96d6baee4 --- /dev/null +++ b/src/lib/sdk/billing.ts @@ -0,0 +1,961 @@ +import type { Client, Models, Query } from '@appwrite.io/console'; +import type { Organization } from '../stores/organization'; +import type { PaymentMethod } from '@stripe/stripe-js'; +import type { Tier } from '$lib/stores/billing'; + +export type PaymentMethodData = { + $id: string; + $createdAt: string; + $updatedAt: string; + providerMethodId: string; + providerUserId: string; + expiryMonth: number; + expiryYear: number; + expired: boolean; + last4: string; + country: string; + brand: string; + clientSecret: string; + failed: boolean; + name: string; +}; + +export type PaymentList = { + paymentMethods: PaymentMethodData[]; + total: number; +}; + +export type Invoice = { + $id: string; + $createdAt: Date; + $updatedAt: Date; + teamId: string; + aggregationId: string; + amount: number; + currency: string; + date: number; + from: string; + to: string; + status: string; + dueAt: string; + clientSecret: string; + tier: Tier; + usage: { + name: string; + value: number /* service over the limit*/; + amount: number /* price of service over the limit*/; + rate: number; + desc: string; + }[]; +}; + +export type InvoiceList = { + invoices: Invoice[]; + total: number; +}; + +export type Coupon = { + $id: string; + code: string; + credits: number; + expiration: string; + status: string; // 'active' | 'disabled' | 'expired' + validity: number; +}; + +export type Credit = { + /** + * Credit ID. + */ + $id: string; + /** + * Credit creation time in ISO 8601 format. + */ + $createdAt: string; + /** + * Credit update date in ISO 8601 format. + */ + $updatedAt: string; + /** + * coupon ID + */ + couponId: string; + /** + * ID of the User. + */ + userId: string; + /** + * ID of the Team. + */ + teamId: string; + /** + * Provided credit amount + */ + credits: number; + /** + * Remaining up credit amount + */ + creditsRemaining: number; + /** + * Credit expiration time in ISO 8601 format. + */ + expiration: string; + /** + * Status of the credit. Can be one of `disabled`, `active` or `expired`. + */ + status: string; +}; + +export type CreditList = { + credits: Credit[]; + total: number; +}; + +export type Aggregation = { + $id: string; + /** + * Aggregation creation time in ISO 8601 format. + */ + $createdAt: string; + /** + * Aggregation update date in ISO 8601 format. + */ + $updatedAt: string; + /** + * Beginning date of the invoice. + */ + from: string; + /** + * End date of the invoice. + */ + to: string; + /** + * Total amount of the invoice. + */ + amount: number; + /** + * Price for addional members + */ + additionalMembers: number; + /** + * Total storage usage. + */ + usageStorage: number; + /** + * Total active users for the billing period. + */ + usageUsers: number; + /** + * Total number of executions for the billing period. + */ + usageExecutions: number; + /** + * Total bandwidth usage for the billing period. + */ + usageBandwidth: number; + /** + * Total realtime usage for the billing period. + */ + usageRealtime: number; + /** + * Usage logs for the billing period. + */ + resources: OrganizationUsage; +}; + +export type OrganizationUsage = { + bandwidth: Array; + executions: Array; + executionsTotal: number; + storageTotal: number; + users: Array; + usersTotal: number; + projects: Array<{ + projectId: string; + storage: number; + executions: number; + bandwidth: number; + users: number; + }>; +}; + +export type AggregationList = { + aggregations: Aggregation[]; + total: number; +}; + +export type AllowedRegions = + | 'fra' + | 'nyc' + | 'sfo' + | 'blr' + | 'lon' + | 'ams' + | 'sgp' + | 'tor' + | 'syd' + | 'default'; //TODO: remove after migration + +export type Region = { + $id: AllowedRegions; + name: string; + disabled: boolean; + default: boolean; + flag: string; +}; + +export type RegionList = { + regions: Region[]; + total: number; +}; + +export type Address = { + $id: string; + streetAddress: string; + addressLine2?: string; + country: string; + city: string; + state?: string; + postalCode: string; +}; + +export type AddressesList = { + billingAddresses: Address[]; + total: number; +}; + +export type AdditionalResource = { + currency: string; + invoiceDesc: string; + price: number; + unit: string; + value: number; + multiplier?: number; +}; + +export type Plan = { + $id: string; + name: string; + price: number; + bandwidth: number; + storage: number; + members: number; + webhooks: number; + users: number; + teams: number; + databases: number; + buckets: number; + fileSize: number; + functions: number; + executions: number; + realtime: number; + logs: number; + addons: { + bandwidth: AdditionalResource; + executions: AdditionalResource; + member: AdditionalResource; + realtime: AdditionalResource; + storage: AdditionalResource; + users: AdditionalResource; + }; +}; + +export type PlansInfo = { + plans: Plan[]; + total: number; +}; + +export type PlansMap = Map; + +export class Billing { + client: Client; + + constructor(client: Client) { + this.client = client; + } + + async createOrganization( + organizationId: string, + name: string, + billingPlan: string, + paymentMethodId: string, + billingAddressId: string + ): Promise { + const path = `/organizations`; + const params = { + organizationId, + name, + billingPlan, + paymentMethodId, + billingAddressId + }; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call( + 'POST', + uri, + { + 'content-type': 'application/json' + }, + params + ); + } + + async deleteOrganization(organizationId: string): Promise { + const path = `/organizations/${organizationId}`; + const params = { + organizationId + }; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call( + 'DELETE', + uri, + { + 'content-type': 'application/json' + }, + params + ); + } + + async getPlan(organizationId: string): Promise { + const path = `/organizations/${organizationId}/plan`; + const params = { + organizationId + }; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call( + 'get', + uri, + { + 'content-type': 'application/json' + }, + params + ); + } + + async updatePlan( + organizationId: string, + billingPlan: string, + paymentMethodId: string, + billingAddressId: string + ): Promise { + const path = `/organizations/${organizationId}/plan`; + const params = { + organizationId, + billingPlan, + paymentMethodId, + billingAddressId + }; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call( + 'patch', + uri, + { + 'content-type': 'application/json' + }, + params + ); + } + + async updateBudget( + organizationId: string, + budget: number, + alerts: number[] + ): Promise { + const path = `/organizations/${organizationId}/budget`; + const params = { + organizationId, + budget, + alerts + }; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call( + 'patch', + uri, + { + 'content-type': 'application/json' + }, + params + ); + } + + async setOrganizationPaymentMethod( + organizationId: string, + paymentMethodId: string + ): Promise { + const path = `/organizations/${organizationId}/payment-method`; + const params = { + organizationId, + paymentMethodId + }; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call( + 'patch', + uri, + { + 'content-type': 'application/json' + }, + params + ); + } + + async setOrganizationPaymentMethodBackup( + organizationId: string, + paymentMethodId: string + ): Promise { + const path = `/organizations/${organizationId}/payment-method/backup`; + const params = { + organizationId, + paymentMethodId + }; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call( + 'patch', + uri, + { + 'content-type': 'application/json' + }, + params + ); + } + async removeOrganizationPaymentMethod(organizationId: string): Promise { + const path = `/organizations/${organizationId}/payment-method`; + const params = { + organizationId + }; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call( + 'DELETE', + uri, + { + 'content-type': 'application/json' + }, + params + ); + } + async removeOrganizationPaymentMethodBackup(organizationId: string): Promise { + const path = `/organizations/${organizationId}/payment-method/backup`; + const params = { + organizationId + }; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call( + 'DELETE', + uri, + { + 'content-type': 'application/json' + }, + params + ); + } + + async listInvoices(organizationId: string, queries: Query[] = []): Promise { + const path = `/organizations/${organizationId}/invoices`; + const params = { + organizationId, + queries + }; + + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call( + 'get', + uri, + { + 'content-type': 'application/json' + }, + params + ); + } + + async getInvoice(organizationId: string, invoiceId: string): Promise { + const path = `/organizations/${organizationId}/invoices/${invoiceId}`; + const params = { + organizationId, + invoiceId + }; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call( + 'get', + uri, + { + 'content-type': 'application/json' + }, + params + ); + } + + async getInvoiceView( + organizationId: string, + invoiceId: string + ): Promise> { + const path = `/organizations/${organizationId}/invoices/${invoiceId}/view`; + const params = { + organizationId, + invoiceId + }; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call( + 'get', + uri, + { + 'content-type': 'application/pdf' + }, + params + ); + } + + async downloadInvoice( + organizationId: string, + invoiceId: string + ): Promise> { + const path = `/organizations/${organizationId}/invoices/${invoiceId}/download`; + const params = { + organizationId, + invoiceId + }; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call( + 'get', + uri, + { + 'content-type': 'application/json' + }, + params + ); + } + + async listUsage( + organizationId: string, + startDate: string = undefined, + endDate: string = undefined + ): Promise { + const path = `/organizations/${organizationId}/usage`; + const params = { + organizationId + }; + + if (startDate !== undefined) { + params['startDate'] = startDate; + } + if (endDate !== undefined) { + params['endDate'] = endDate; + } + + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call( + 'get', + uri, + { + 'content-type': 'application/json' + }, + params + ); + } + + async listAggregation( + organizationId: string, + queries: string[] = [] + ): Promise { + const path = `/organizations/${organizationId}/aggregations`; + const params = { + organizationId, + queries + }; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call( + 'get', + uri, + { + 'content-type': 'application/json' + }, + params + ); + } + + async getAggregation(organizationId: string, aggregationId: string): Promise { + const path = `/organizations/${organizationId}/aggregations/${aggregationId}`; + const params = { + organizationId, + aggregationId + }; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call( + 'get', + uri, + { + 'content-type': 'application/json' + }, + params + ); + } + + async addCredit(organizationId: string, couponId: string): Promise { + const path = `/organizations/${organizationId}/credits`; + const params = { + couponId + }; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call( + 'POST', + uri, + { + 'content-type': 'application/json' + }, + params + ); + } + async listCredits(organizationId: string, queries = []): Promise { + const path = `/organizations/${organizationId}/credits`; + const params = { + queries + }; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call( + 'get', + uri, + { + 'content-type': 'application/json' + }, + params + ); + } + + async getCredit(organizationId: string, creditId: string): Promise { + const path = `/organizations/${organizationId}/credits/${creditId}`; + const params = { + creditId + }; + 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 = { + couponId + }; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call( + 'GET', + uri, + { + 'content-type': 'application/json' + }, + params + ); + } + + async setBillingAddress( + organizationId: string, + billingAddressId: string + ): Promise { + const path = `/organizations/${organizationId}/billing-address`; + const params = { + organizationId, + billingAddressId + }; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call( + 'PATCH', + uri, + { + 'content-type': 'application/json' + }, + params + ); + } + + async removeBillingAddress(organizationId: string): Promise { + const path = `/organizations/${organizationId}/billing-address`; + const params = { + organizationId + }; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call( + 'DELETE', + uri, + { + 'content-type': 'application/json' + }, + params + ); + } + + async updateTaxId(organizationId: string, taxId: string): Promise { + const path = `/organizations/${organizationId}/taxId`; + const params = { + organizationId, + taxId + }; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call( + 'PATCH', + uri, + { + 'content-type': 'application/json' + }, + params + ); + } + + //ACCOUNT + + async listPaymentMethods(queries: [] = []): Promise { + const path = `/account/payment-methods`; + const params = { + queries + }; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call( + 'GET', + uri, + { + 'content-type': 'application/json' + }, + params + ); + } + + async getPaymentMethod(paymentMethodId: string): Promise { + const path = `/account/payment-methods/${paymentMethodId}`; + const params = { + paymentMethodId + }; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call( + 'GET', + uri, + { + 'content-type': 'application/json' + }, + params + ); + } + + async createPaymentMethod(): Promise { + const path = `/account/payment-methods`; + const params = {}; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call( + 'POST', + uri, + { + 'content-type': 'application/json' + }, + params + ); + } + + async setPaymentMethod( + paymentMethodId: string, + providerMethodId: string | PaymentMethod, + name: string + ): Promise { + const path = `/account/payment-methods/${paymentMethodId}/provider`; + const params = { + paymentMethodId, + providerMethodId, + name + }; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call( + 'patch', + uri, + { + 'content-type': 'application/json' + }, + params + ); + } + + async updatePaymentMethod( + paymentMethodId: string, + expiryMonth: string, + expiryYear: string + ): Promise { + const path = `/account/payment-methods/${paymentMethodId}`; + const params = { + paymentMethodId, + expiryMonth, + expiryYear + }; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call( + 'patch', + uri, + { + 'content-type': 'application/json' + }, + params + ); + } + + async deletePaymentMethod(paymentMethodId: string): Promise { + const path = `/account/payment-methods/${paymentMethodId}`; + const params = { + paymentMethodId + }; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call( + 'delete', + uri, + { + 'content-type': 'application/json' + }, + params + ); + } + async setDefaultPaymentMethod(paymentMethodId: string): Promise { + const path = `/account/payment-methods/${paymentMethodId}/default`; + const params = { + paymentMethodId + }; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call( + 'patch', + uri, + { + 'content-type': 'application/json' + }, + params + ); + } + + async listAddresses(queries: Query[] = []): Promise { + const path = `/account/billing-addresses`; + const params = { + queries + }; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call( + 'get', + uri, + { + 'content-type': 'application/json' + }, + params + ); + } + + async getAddress(billingAddressId: string): Promise
    { + const path = `/account/billing-addresses/${billingAddressId}`; + const params = { + billingAddressId + }; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call( + 'get', + uri, + { + 'content-type': 'application/json' + }, + params + ); + } + + async createAddress( + country: string, + streetAddress: string, + city: string, + state: string, + postalCode: string, + addressLine2?: string + ): Promise
    { + const path = `/account/billing-addresses`; + const params = { + country, + streetAddress, + city, + state, + postalCode, + addressLine2 + }; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call( + 'POST', + uri, + { + 'content-type': 'application/json' + }, + params + ); + } + async updateAddress( + billingAddressId: string, + country: string, + streetAddress: string, + city: string, + state: string, + postalCode: string, + addressLine2?: string + ): Promise
    { + const path = `/account/billing-addresses/${billingAddressId}`; + const params = { + billingAddressId, + country, + streetAddress, + city, + state, + postalCode, + addressLine2 + }; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call( + 'PUT', + uri, + { + 'content-type': 'application/json' + }, + params + ); + } + async deleteAddress(billingAddressId: string): Promise { + const path = `/account/billing-addresses/${billingAddressId}`; + const params = { + billingAddressId + }; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call( + 'delete', + uri, + { + 'content-type': 'application/json' + }, + params + ); + } + + async listRegions(): Promise { + const path = `/console/regions`; + const params = {}; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call( + 'GET', + uri, + { + 'content-type': 'application/json' + }, + params + ); + } + + async getPlansInfo(): Promise { + const path = `/console/plans`; + const params = {}; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call( + 'GET', + uri, + { + 'content-type': 'application/json' + }, + params + ); + } +} diff --git a/src/lib/sdk/usage.ts b/src/lib/sdk/usage.ts index f0837ef02..f47e14341 100644 --- a/src/lib/sdk/usage.ts +++ b/src/lib/sdk/usage.ts @@ -1,3 +1,23 @@ +import type { Models } from '@appwrite.io/console'; + +export function accumulateUsage(usage: Models.Metric[], base: number): Models.Metric[] { + const accumulation = usage.reduce( + (carry, item) => { + const value = item.value + carry.currentTotal; + return { + currentTotal: value, + metrics: [...carry.metrics, { ...item, value }] + }; + }, + { + currentTotal: base, + metrics: [] + } + ); + + return accumulation.metrics; +} + export type Metric = { /** * The value of this metric at the timestamp. diff --git a/src/lib/stores/billing.ts b/src/lib/stores/billing.ts new file mode 100644 index 000000000..a86f8c8ae --- /dev/null +++ b/src/lib/stores/billing.ts @@ -0,0 +1,300 @@ +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 } from '$lib/sdk/billing'; +import { isCloud } from '$lib/system'; +import { cachedStore } from '$lib/helpers/cache'; +import { Query, type Models } from '@appwrite.io/console'; +import { headerAlert } from './headerAlert'; +import PaymentAuthRequired from '$lib/components/billing/alerts/paymentAuthRequired.svelte'; +import { addNotification, notifications } from './notifications'; +import { goto } from '$app/navigation'; +import { base } from '$app/paths'; +import TooManyFreOrgs from '$lib/components/billing/alerts/tooManyFreeOrgs.svelte'; +import { activeHeaderAlert, showPostReleaseModal } from '$routes/console/store'; +import MarkedForDeletion from '$lib/components/billing/alerts/markedForDeletion.svelte'; +import { BillingPlan } from '$lib/constants'; + +export type Tier = 'tier-0' | 'tier-1' | 'tier-2'; + +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 function tierToPlan(tier: Tier) { + switch (tier) { + case BillingPlan.STARTER: + return tierFree; + case BillingPlan.PRO: + return tierPro; + case BillingPlan.SCALE: + return tierScale; + default: + return tierFree; + } +} + +export type PlanServices = + | 'bandwidth' + | 'bandwidthAddon' + | 'buckets' + | 'databases' + | 'executions' + | 'executionsAddon' + | 'fileSize' + | 'functions' + | 'logs' + | 'memberAddon' + | 'members' + | 'platforms' + | 'realtime' + | 'realtimeAddon' + | 'storage' + | 'storageAddon' + | 'teams' + | 'users' + | 'usersAddon' + | 'webhooks'; + +export function getServiceLimit(serviceId: PlanServices, tier: Tier = null): number { + if (!isCloud) return 0; + if (!serviceId) return 0; + const info = get(plansInfo); + if (!info) return 0; + const plan = info.get(tier ?? get(organization)?.billingPlan); + return plan?.[serviceId]; +} + +export const failedInvoice = cachedStore< + false | Invoice, + { + load: (orgId: string) => Promise; + } +>('failedInvoice', function ({ set }) { + return { + load: async (orgId) => { + if (!isCloud) set(false); + const invoices = await sdk.forConsole.billing.listInvoices(orgId); + const failedInvoices = invoices.invoices.filter((i) => i.status === 'failed'); + // const failedInvoices = invoices.invoices; + if (failedInvoices.length > 0) { + const firstFailed = failedInvoices[0]; + const today = new Date(); + const thirtyDaysAgo = new Date(today.setDate(today.getDate() - 30)); + const failedDate = new Date(firstFailed.$createdAt); + if (failedDate < thirtyDaysAgo) { + readOnly.set(true); + } + } else set(false); + } + }; +}); + +export const actionRequiredInvoices = writable(null); + +export type TierData = { + name: string; + description: string; +}; + +export const tierFree: TierData = { + name: 'Starter', + description: 'For personal, passion projects.' +}; + +export const tierPro: TierData = { + name: 'Pro', + description: 'For pro developers and production projects that need the ability to scale.' +}; +export const tierScale: TierData = { + name: 'Scale', + description: 'For scaling teams that need dedicated support.' +}; + +export const showUsageRatesModal = writable(false); + +export function checkForUsageFees(plan: Tier, id: PlanServices) { + if (plan === BillingPlan.PRO || plan === BillingPlan.SCALE) { + switch (id) { + case 'bandwidth': + case 'storage': + case 'users': + case 'executions': + case 'realtime': + return true; + + default: + return false; + } + } else return false; +} + +export function checkForProjectLimitation(id: PlanServices) { + switch (id) { + case 'databases': + case 'functions': + case 'buckets': + case 'members': + case 'platforms': + case 'webhooks': + case 'teams': + return true; + + default: + return false; + } +} + +export function isServiceLimited(serviceId: PlanServices, plan: Tier, total: number) { + if (!total) return false; + const limit = getServiceLimit(serviceId) || Infinity; + const isLimited = limit !== 0 && limit < Infinity; + const hasUsageFees = checkForUsageFees(plan, serviceId); + return isLimited && total >= limit && !hasUsageFees; +} + +export function calculateTrialDay(org: Organization) { + if (org?.billingPlan === BillingPlan.STARTER) return false; + const endDate = new Date(org?.billingStartDate); + const today = new Date(); + + let diffTime = endDate.getTime() - today.getTime(); + diffTime = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1; + + const days = diffTime < 1 ? 0 : diffTime; + + daysLeftInTrial.set(days); + return days; +} + +export async function checkForUsageLimit(org: Organization) { + if (!org?.billingLimits) { + readOnly.set(false); + return; + } + const { bandwidth, documents, executions, storage, users } = org?.billingLimits ?? {}; + const members = await sdk.forConsole.teams.listMemberships(org.$id); + const plan = get(plansInfo)?.get(org.billingPlan); + const membersOverflow = + members?.total > plan.members ? members.total - (plan.members || members.total) : 0; + + if ( + bandwidth >= 100 || + documents >= 100 || + executions >= 100 || + storage >= 100 || + users >= 100 || + membersOverflow > 0 + ) { + readOnly.set(true); + } else readOnly.set(false); +} + +export async function checkPaymentAuthorizationRequired(org: Organization) { + if (org.billingPlan === BillingPlan.STARTER) return; + + const invoices = await sdk.forConsole.billing.listInvoices(org.$id, [ + Query.equal('status', 'requires_authentication') + ]); + + if (invoices?.invoices?.length > 0) { + headerAlert.add({ + id: 'paymentAuthRequired', + component: PaymentAuthRequired, + show: true, + importance: 8 + }); + } + activeHeaderAlert.set(headerAlert.get()); + + actionRequiredInvoices.set(invoices); +} + +export async function paymentExpired(org: Organization) { + if (!org?.paymentMethodId) return; + const payment = await sdk.forConsole.billing.getPaymentMethod(org.paymentMethodId); + if (!payment?.expiryYear) return; + const year = new Date().getFullYear(); + const month = new Date().getMonth(); + const expiredMessage = `The default payment method for ${org.name} has expired`; + const expiringMessage = `The default payment method for ${org.name} will expire soon`; + const nots = get(notifications); + const expiredNotification = nots.some((n) => n.message === expiredMessage); + const expiringNotification = nots.some((n) => n.message === expiringMessage); + if (payment.expired && !expiredNotification) { + addNotification({ + type: 'error', + isHtml: true, + timeout: 0, + message: expiredMessage, + buttons: [ + { + name: 'Update payment details', + method: () => { + goto(`${base}/console/account/payments`); + } + } + ] + }); + } else if (!expiringNotification && payment.expiryYear <= year && payment.expiryMonth < month) { + addNotification({ + type: 'warning', + isHtml: true, + message: expiringMessage, + buttons: [ + { + name: 'Update payment details', + method: () => { + goto(`${base}/console/account/payments`); + } + } + ] + }); + } +} + +export function checkForMarkedForDeletion(org: Organization) { + if (org?.markedForDeletion) { + headerAlert.add({ + id: 'markedForDeletion', + component: MarkedForDeletion, + show: true, + importance: 5 + }); + } +} + +export async function checkForFreeOrgOverflow(orgs: Models.TeamList>) { + if (orgs?.teams?.length > 1) { + headerAlert.add({ + id: 'freeOrgOverflow', + component: TooManyFreOrgs, + show: true, + importance: 10 + }); + activeHeaderAlert.set(headerAlert.get()); + } +} + +export async function checkForPostReleaseProModal(orgs: Models.TeamList>) { + if (!orgs?.teams?.length) return; + if (orgs.total > orgs.teams.length) return; // if the total is greater that the free orgs it means that there are pro orgs + const modalTime = localStorage.getItem('postReleaseProModal'); + const now = Date.now(); + // show the modal if it was never shown + if (!modalTime) { + localStorage.setItem('postReleaseProModal', Date.now().toString()); + showPostReleaseModal.set(true); + } else { + const interval = 5 * 24 * 60 * 60 * 1000; + const sinceLastModal = now - parseInt(modalTime); + // show the modal if it was shown more than 5 days ago + if (sinceLastModal > interval) { + localStorage.setItem('postReleaseProModal', Date.now().toString()); + showPostReleaseModal.set(true); + } + } +} diff --git a/src/lib/stores/headerAlert.ts b/src/lib/stores/headerAlert.ts new file mode 100644 index 000000000..2f5ed499f --- /dev/null +++ b/src/lib/stores/headerAlert.ts @@ -0,0 +1,54 @@ +import type { SvelteComponent } from 'svelte'; +import { writable } from 'svelte/store'; + +export type HeaderAlert = { + id: string; + show: boolean; + component: typeof SvelteComponent; + importance: number; +}; + +export type HeaderAlertStore = { + components: HeaderAlert[]; +}; + +function createHeaderAlertStore() { + const { subscribe, update, set } = writable({ + components: [] + }); + + return { + subscribe, + set, + add: (component: HeaderAlert) => { + update((n) => { + if (n.components.some((c) => c.id === component.id)) return n; + else { + n.components.push(component); + n.components = [...n.components]; + return n; + } + }); + }, + get: (): HeaderAlert => { + //return higest importance component + let component = { + id: '', + show: false, + component: null, + importance: 0 + }; + update((n) => { + n.components.forEach((c) => { + if (c.importance > component.importance) { + component = c; + } + }); + return n; + }); + return component as HeaderAlert; + } + }; +} + +export const headerAlert = createHeaderAlertStore(); diff --git a/src/lib/stores/marketplace.ts b/src/lib/stores/marketplace.ts index 1cb068b38..5e74be642 100644 --- a/src/lib/stores/marketplace.ts +++ b/src/lib/stores/marketplace.ts @@ -37,7 +37,7 @@ export const marketplace: MarketplaceTemplate[] = [ id: 'starter', name: 'Starter function', tagline: - 'A simple function to get started. Edit this function to explore endless possibilities with Appwrite Functions. 🚀', + 'A simple function to get started. Edit this function to explore endless possibilities with Appwrite Functions.', permissions: ['any'], events: [], cron: '', diff --git a/src/lib/stores/notifications.ts b/src/lib/stores/notifications.ts index 616c85832..97f6cfb36 100644 --- a/src/lib/stores/notifications.ts +++ b/src/lib/stores/notifications.ts @@ -9,6 +9,7 @@ export type Notification = { title?: string; icon?: string; buttons?: Buttons[]; + isHtml?: boolean; }; export type Buttons = { diff --git a/src/lib/stores/organization.ts b/src/lib/stores/organization.ts index 82fc5aae8..38914e5b0 100644 --- a/src/lib/stores/organization.ts +++ b/src/lib/stores/organization.ts @@ -1,6 +1,34 @@ import { page } from '$app/stores'; import { derived, writable } from 'svelte/store'; import type { Models } from '@appwrite.io/console'; +import type { Tier } from './billing'; + +export type Organization = Models.Team> & { + billingBudget: number; + billingPlan: Tier; + budgetAlerts: number[]; + paymentMethodId: string; + backupPaymentMethodId: string; + markedForDeletion: boolean; + billingLimits: BillingLimits; + billingCurrentInvoiceDate: string; + billingNextInvoiceDate: string; + billingTrialStartDate?: string; + billingStartDate?: string; + billingTrialDays?: number; + billingAddressId?: string; + amount: number; + billingTaxId?: string; + billingPlanDowngrade?: string; +}; + +export type BillingLimits = { + bandwidth: number; + documents: number; + executions: number; + storage: number; + users: number; +}; export const newOrgModal = writable(false); export const newMemberModal = writable(false); @@ -8,8 +36,5 @@ export const organizationList = derived( page, ($page) => $page.data.organizations as Models.TeamList> ); -export const organization = derived( - page, - ($page) => $page.data?.organization as Models.Team> -); +export const organization = derived(page, ($page) => $page.data?.organization as Organization); export const members = derived(page, ($page) => $page.data.members as Models.MembershipList); diff --git a/src/lib/stores/sdk.ts b/src/lib/stores/sdk.ts index 673be315e..caacafee7 100644 --- a/src/lib/stores/sdk.ts +++ b/src/lib/stores/sdk.ts @@ -20,6 +20,7 @@ import { Users, Vcs } from '@appwrite.io/console'; +import { Billing } from '../sdk/billing'; const endpoint = VARS.APPWRITE_ENDPOINT ?? `${globalThis?.location?.origin}/v1`; @@ -28,6 +29,7 @@ clientConsole.setEndpoint(endpoint).setProject('console'); const clientProject = new Client(); clientProject.setEndpoint(endpoint).setMode('admin'); + const sdkForProject = { client: clientProject, account: new Account(clientProject), @@ -67,7 +69,8 @@ export const sdk = { users: new Users(clientConsole), migrations: new Migrations(clientConsole), console: new Console(clientConsole), - assistant: new Assistant(clientConsole) + assistant: new Assistant(clientConsole), + billing: new Billing(clientConsole) }, get forProject() { const projectId = getProjectId(); diff --git a/src/lib/stores/stripe.ts b/src/lib/stores/stripe.ts new file mode 100644 index 000000000..963adcf9e --- /dev/null +++ b/src/lib/stores/stripe.ts @@ -0,0 +1,186 @@ +import type { Stripe, StripeElement, StripeElements } from '@stripe/stripe-js'; +import { sdk } from './sdk'; +import { app } from './app'; +import { get, writable } from 'svelte/store'; +import type { PaymentMethodData } from '$lib/sdk/billing'; +import { Submit, trackError, trackEvent } from '$lib/actions/analytics'; +import { addNotification } from './notifications'; + +export const stripe = writable(); +let paymentMethod: PaymentMethodData; +let clientSecret: string; +let elements: StripeElements; +let paymentElement: StripeElement; + +export const isStripeInitialized = writable(false); + +export async function initializeStripe() { + if (!get(stripe)) return; + isStripeInitialized.set(true); + + const methods = await sdk.forConsole.billing.listPaymentMethods(); + clientSecret = methods.paymentMethods[0]?.clientSecret; + // If there is no payment method, create an empty one and get the client secret + if (!clientSecret) { + paymentMethod = await sdk.forConsole.billing.createPaymentMethod(); + clientSecret = paymentMethod.clientSecret; + } + + // Set up the options for the stripe elements + const options = { + clientSecret: clientSecret, + appearance: get(app).themeInUse === 'dark' ? apperanceDark : apperanceLight + }; + // Set up Elements and then create form + elements = get(stripe).elements(options); + paymentElement = elements.create('payment'); + paymentElement.mount('#payment-element'); +} + +export async function unmountPaymentElement() { + isStripeInitialized.set(false); + paymentElement?.unmount(); + clientSecret = null; + paymentMethod = null; + elements = null; +} + +export async function submitStripeCard(name: string, urlRoute?: string) { + try { + // If a payment method was created during initialization, use it, otherwise create a new one + if (!paymentMethod) { + paymentMethod = await sdk.forConsole.billing.createPaymentMethod(); + clientSecret = paymentMethod.clientSecret; + } + + // // Element needs to be submitted before confirming the setup intent + elements.submit(); + + const baseUrl = 'https://cloud.appwrite.io/console/'; + + const { setupIntent, error } = await get(stripe).confirmSetup({ + elements, + clientSecret, + confirmParams: { + return_url: `${baseUrl}${ + urlRoute ?? `organization/billing?clientSecret=${clientSecret}` + }`, + payment_method_data: { + billing_details: { + name + } + } + }, + redirect: 'if_required' + }); + + if (error) { + const e = new Error(error.message); + trackError(e, Submit.PaymentMethodCreate); + throw e; + } + + if (setupIntent && setupIntent.status === 'succeeded') { + const method = await sdk.forConsole.billing.setPaymentMethod( + paymentMethod.$id, + setupIntent.payment_method, + name + ); + paymentElement.destroy(); + isStripeInitialized.set(false); + trackEvent(Submit.PaymentMethodCreate); + return method; + } else { + const e = new Error('Something went wrong'); + trackError(e, Submit.PaymentMethodCreate); + throw e.message; + } + } catch (e) { + trackError(e, Submit.PaymentMethodCreate); + throw e; + } +} + +export async function confirmPayment(orgId: string, clientSecret: string, paymentMethodId: string) { + try { + const url = `${window.location.origin}/console/organization-${orgId}/billing`; + + const paymentMethod = await sdk.forConsole.billing.getPaymentMethod(paymentMethodId); + + const { error } = await get(stripe).confirmPayment({ + clientSecret: clientSecret, + confirmParams: { + return_url: url, + payment_method: paymentMethod.providerMethodId + } + }); + if (error) { + throw error.message; + } + } catch (e) { + addNotification({ + title: 'Error', + message: + 'There was an error processing your payment, try again later. If the problem persists, please contact support.', + type: 'error' + }); + } +} + +const apperanceLight = { + variables: { + colorPrimary: '#606a7b', + colorText: 'rgb(107, 107, 112)', + colorBackground: '#FFFFFF', + color: '#606a7b', + colorDanger: '#df1b41', + fontFamily: 'Inter, arial, sans-serif', + borderRadius: '4px' + }, + rules: { + '.Input:hover': { + border: 'solid 1px rgb(195, 195, 198)', + boxShadow: 'none' + }, + '.Input:focus': { + border: 'solid 1px rgb(195, 195, 198)', + boxShadow: 'none' + }, + '.Input::placeholder': { + color: '#C4C6D7' + }, + '.Input--invalid': { + border: 'solid 1px var(--colorDanger)', + boxShadow: 'none' + } + } +}; + +const apperanceDark = { + variables: { + colorPrimary: '#606a7b', + colorText: 'rgb(195, 195, 198)', + colorBackground: 'rgb(24, 24, 27)', + colorDanger: '#FF453A', + fontFamily: 'Inter, arial, sans-serif', + borderRadius: '4px', + spacingGridRow: '16px' + }, + rules: { + '.Input:hover': { + border: 'solid 1px rgb(87, 87, 92)', + boxShadow: 'none' + }, + '.Input:focus': { + border: 'solid 1px rgb(87, 87, 92)', + boxShadow: 'none' + }, + '.Input::placeholder': { + color: 'rgb(87, 87, 92)' + }, + '.Input--invalid': { + border: 'solid 1px var(--colorDanger)', + boxShadow: 'none' + } + } +}; diff --git a/src/lib/stores/wizard.ts b/src/lib/stores/wizard.ts index 5f3752301..6a752584f 100644 --- a/src/lib/stores/wizard.ts +++ b/src/lib/stores/wizard.ts @@ -1,4 +1,5 @@ import { trackEvent } from '$lib/actions/analytics'; +import type { WizardStepsType } from '$lib/layout/wizard.svelte'; import type { SvelteComponent } from 'svelte'; import { writable } from 'svelte/store'; @@ -8,8 +9,10 @@ export type WizardStore = { component?: typeof SvelteComponent; cover?: typeof SvelteComponent; interceptor?: () => Promise; + finalAction?: () => Promise; nextDisabled: boolean; step: number; + interceptorNotificationEnabled: boolean; }; function createWizardStore() { @@ -18,22 +21,31 @@ function createWizardStore() { component: null, cover: null, interceptor: null, + interceptorNotificationEnabled: true, media: null, nextDisabled: false, - step: 1 + step: 1, + finalAction: null }); return { subscribe, set, - start: (component: typeof SvelteComponent, media: string = null) => + start: ( + component: typeof SvelteComponent, + media: string = null, + step: number = 1 + ) => update((n) => { n.show = true; n.component = component; n.interceptor = null; + n.interceptorNotificationEnabled = true; n.media = media; - n.step = 1; + n.step = step; n.cover = null; + n.nextDisabled = false; + n.finalAction = null; trackEvent('wizard_start'); return n; }), @@ -54,9 +66,13 @@ function createWizardStore() { n.show = false; n.component = null; n.interceptor = null; + n.interceptorNotificationEnabled = true; n.media = null; n.step = 1; n.cover = null; + n.nextDisabled = false; + n.finalAction = null; + return n; }), showCover: (component: typeof SvelteComponent) => @@ -80,3 +96,12 @@ function createWizardStore() { } export const wizard = createWizardStore(); + +export function updateStepStatus(map: WizardStepsType, key: number, status: boolean) { + const updatedComponent = { + ...map.get(key), + disabled: status + }; + map.set(key, updatedComponent); + return map; +} diff --git a/src/lib/system.ts b/src/lib/system.ts index eaf4404b4..16e26d13f 100644 --- a/src/lib/system.ts +++ b/src/lib/system.ts @@ -8,7 +8,8 @@ export const VARS = { GROWTH_ENDPOINT: import.meta.env?.VITE_APPWRITE_GROWTH_ENDPOINT?.toString() as | string | undefined, - CONSOLE_MODE: import.meta.env?.VITE_CONSOLE_MODE?.toString() as string | undefined + CONSOLE_MODE: import.meta.env?.VITE_CONSOLE_MODE?.toString() as string | undefined, + STRIPE_PUBLIC_KEY: import.meta.env?.VITE_STRIPE_PUBLIC_KEY?.toString() as string | undefined }; export const ENV = { @@ -21,3 +22,5 @@ export const ENV = { export const MODE = VARS.CONSOLE_MODE === Mode.CLOUD ? Mode.CLOUD : Mode.SELF_HOSTED; export const isCloud = MODE === Mode.CLOUD; export const isSelfHosted = MODE !== Mode.CLOUD; +export const hasStripePublicKey = !!VARS.STRIPE_PUBLIC_KEY; +export const GRACE_PERIOD_OVERRIDE = true; diff --git a/src/lib/wizards/functions/components/repositories.svelte b/src/lib/wizards/functions/components/repositories.svelte index ee2dfdfe0..f4f76872d 100644 --- a/src/lib/wizards/functions/components/repositories.svelte +++ b/src/lib/wizards/functions/components/repositories.svelte @@ -87,13 +87,13 @@ ]} value={null} />
    -
    +
      -
    + {:then installations}
    -
    +
      -
    -
    + +
      -
    +
    {/await}

    diff --git a/src/lib/wizards/functions/cover.svelte b/src/lib/wizards/functions/cover.svelte index ab119b76a..bcc19f123 100644 --- a/src/lib/wizards/functions/cover.svelte +++ b/src/lib/wizards/functions/cover.svelte @@ -93,7 +93,7 @@

    - {#if isSelfHosted || !isVcsEnabled} + {#if isSelfHosted && !isVcsEnabled}
    diff --git a/src/lib/wizards/functions/steps/functionConfiguration.svelte b/src/lib/wizards/functions/steps/functionConfiguration.svelte index ba201d88c..a0209dd8d 100644 --- a/src/lib/wizards/functions/steps/functionConfiguration.svelte +++ b/src/lib/wizards/functions/steps/functionConfiguration.svelte @@ -59,6 +59,7 @@ label="Name" id="name" placeholder="Function name" + autofocus bind:value={$createFunction.name} required /> {#if !showCustomId} diff --git a/src/lib/wizards/functions/steps/templateConfiguration.svelte b/src/lib/wizards/functions/steps/templateConfiguration.svelte index 49be494b6..7da879c82 100644 --- a/src/lib/wizards/functions/steps/templateConfiguration.svelte +++ b/src/lib/wizards/functions/steps/templateConfiguration.svelte @@ -39,6 +39,7 @@ label="Name" id="name" placeholder="Function name" + autofocus bind:value={$templateConfig.name} required /> {#await loadRuntimes()} @@ -62,7 +63,7 @@ {/await} -
    + {#if !showCustomId}
    (showCustomId = !showCustomId)}> @@ -77,5 +78,5 @@ bind:id={$templateConfig.$id} fullWidth /> {/if} -
    +
    diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 956cf022e..49e6ea616 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -16,6 +16,7 @@ import Loading from './loading.svelte'; import { loading, requestedMigration } from './store'; import { parseIfString } from '$lib/helpers/object'; + import Consent, { consent } from '$lib/components/consent.svelte'; if (browser) { window.VERCEL_ANALYTICS_ID = import.meta.env.VERCEL_ANALYTICS_ID?.toString() ?? false; @@ -51,7 +52,7 @@ /** * LogRocket */ - if (isCloud && isTrackingAllowed()) { + if ($consent?.accepted?.analytics && isCloud && isTrackingAllowed()) { LogRocket.init('rgthvf/appwrite', { dom: { inputSanitizer: true @@ -124,6 +125,9 @@ +{#if isCloud} + +{/if} @@ -138,7 +142,7 @@ @import '@appwrite.io/pink/src/abstract/variables/_devices.scss'; .tippy-box { --p-tooltip-text-color: var(--color-neutral-10); - --p-tooltip--bg-color: var(--color-neutral-100); + --p-tooltip--bg-color: var(--color-neutral-80); background-color: hsl(var(--p-tooltip--bg-color)); color: hsl(var(--p-tooltip-text-color)); @@ -165,7 +169,7 @@ } .with-separators { - --separator-color: hsl(var(--color-neutral-5)); + --separator-color: hsl(var(--color-neutral-10)); --separator-text: hsl(var(--color-neutral-50)); } diff --git a/src/routes/+layout.ts b/src/routes/+layout.ts index 5bff848e1..1b7b099e2 100644 --- a/src/routes/+layout.ts +++ b/src/routes/+layout.ts @@ -38,7 +38,10 @@ export const load: LayoutLoad = async ({ depends, url }) => { ]; if (!acceptedRoutes.some((n) => url.pathname.startsWith(n))) { - throw redirect(303, '/login'); + const redirectUrl = + url.pathname && url.pathname !== '/' ? `redirect=${url.pathname}` : ''; + const path = url.search ? `${url.search}&${redirectUrl}` : `?${redirectUrl}`; + throw redirect(303, `/login${path}`); } } }; diff --git a/src/routes/console/(billing-modal)/hoodie.png b/src/routes/console/(billing-modal)/hoodie.png new file mode 100644 index 000000000..467ad1af7 Binary files /dev/null and b/src/routes/console/(billing-modal)/hoodie.png differ diff --git a/src/routes/console/(billing-modal)/hoodieCover.svelte b/src/routes/console/(billing-modal)/hoodieCover.svelte new file mode 100644 index 000000000..19fbaa6c5 --- /dev/null +++ b/src/routes/console/(billing-modal)/hoodieCover.svelte @@ -0,0 +1,242 @@ + + + + +
    + + Appwrite + + +
    +
    +
    +
    + +
    + APPWRITE + + PRO +
    + + Your organization has been upgraded 🎉 + +

    + Gear up for your new Pro plan! Share online for a chance to win an exclusive + Appwrite Pro hoodie. +

    + +
    + +
    + +
    + +
    +
    + +
    + +
    +
    +
    +
    + + diff --git a/src/routes/console/(billing-modal)/postReleaseModal.svelte b/src/routes/console/(billing-modal)/postReleaseModal.svelte new file mode 100644 index 000000000..145da0c9a --- /dev/null +++ b/src/routes/console/(billing-modal)/postReleaseModal.svelte @@ -0,0 +1,130 @@ + + + + + {#if $app.themeInUse === 'dark'} + + {:else} + + {/if} + + +
    +

    + Upgrade now and use the code {isNewUser ? 'PRO15' : 'THANKYOU30'} + to claim ${isNewUser ? '15' : '30'} in credits for a Pro plan. Credits will expire on 31 + January 2024. +

    +
    + + Pro: $15 per member per billing period + +

    + For pro developers and teams that need to scale their products. +

    + + {#if freeOrgs?.length > 1 && options?.length} + + + + {:else} +
      +
    • +
    • +
    • +
    • +
    • +
    • +
    • +
    • +
    • +
    • +
    • +
    • +
    + {/if} + +
    +
    diff --git a/src/routes/console/(billing-modal)/side-dark.png b/src/routes/console/(billing-modal)/side-dark.png new file mode 100644 index 000000000..85c816cf6 Binary files /dev/null and b/src/routes/console/(billing-modal)/side-dark.png differ diff --git a/src/routes/console/(billing-modal)/side-light.png b/src/routes/console/(billing-modal)/side-light.png new file mode 100644 index 000000000..22d059de3 Binary files /dev/null and b/src/routes/console/(billing-modal)/side-light.png differ diff --git a/src/routes/console/(migration-wizard)/resource-form.svelte b/src/routes/console/(migration-wizard)/resource-form.svelte index 3524d583a..77572a0b5 100644 --- a/src/routes/console/(migration-wizard)/resource-form.svelte +++ b/src/routes/console/(migration-wizard)/resource-form.svelte @@ -162,8 +162,10 @@
    -

    Keep your project plan limits in mind

    -

    Make sure to have enough storage in your project plan when importing files

    +

    Keep your organization plan's limits in mind

    +

    + Make sure to have enough storage in your organization plan when importing files. +

    {#if $provider.provider === 'firebase'} diff --git a/src/routes/console/+layout.svelte b/src/routes/console/+layout.svelte index bd16a8044..e6cdd038c 100644 --- a/src/routes/console/+layout.svelte +++ b/src/routes/console/+layout.svelte @@ -1,6 +1,6 @@ @@ -264,6 +313,11 @@ !$page.url.pathname.includes('/console/account') && !$page.url.pathname.includes('/console/card') && !$page.url.pathname.includes('/console/onboarding')}> + + {#if $activeHeaderAlert?.show} + + {/if} +
    @@ -281,3 +335,18 @@ {#if $log.show} {/if} + +{#if $showSupportModal} + +{/if} + +{#if isCloud && $showExcess} + +{/if} +{#if isCloud && $showUsageRatesModal} + +{/if} +{#if isCloud && $showPostReleaseModal && !$page.url.pathname.includes('/console/onboarding')} + + +{/if} diff --git a/src/routes/console/+layout.ts b/src/routes/console/+layout.ts index fdcdec07f..3c2496e84 100644 --- a/src/routes/console/+layout.ts +++ b/src/routes/console/+layout.ts @@ -1,6 +1,8 @@ import { Dependencies } from '$lib/constants'; +import type { Plan } from '$lib/sdk/billing'; +import type { Tier } from '$lib/stores/billing'; import { sdk } from '$lib/stores/sdk'; - +import { isCloud } from '$lib/system'; import type { LayoutLoad } from './$types'; export const load: LayoutLoad = async ({ fetch, depends }) => { @@ -18,8 +20,18 @@ export const load: LayoutLoad = async ({ fetch, depends }) => { const [data, variables] = await Promise.all([versionPromise, variablesPromise]); + let plansInfo = new Map(); + if (isCloud) { + const plansArray = await sdk.forConsole.billing.getPlansInfo(); + plansInfo = plansArray.plans.reduce((map, plan) => { + map.set(plan.$id as Tier, plan); + return map; + }, new Map()); + } + return { consoleVariables: variables, - version: data?.version ?? null + version: data?.version ?? null, + plansInfo }; }; diff --git a/src/routes/console/+page.ts b/src/routes/console/+page.ts index 14e5b45e9..3d2e9512a 100644 --- a/src/routes/console/+page.ts +++ b/src/routes/console/+page.ts @@ -2,12 +2,15 @@ import { base } from '$app/paths'; import { redirect } from '@sveltejs/kit'; import type { PageLoad } from './$types'; -export const load: PageLoad = async ({ parent }) => { +export const load: PageLoad = async ({ parent, url }) => { const { organizations, account } = await parent(); + if (organizations.total) { const teamId = account.prefs.organization ?? organizations.teams[0].$id; - throw redirect(303, `${base}/console/organization-${teamId}`); + if (!teamId) { + throw redirect(303, `${base}/console/account/organizations${url.search ?? ''}`); + } else throw redirect(303, `${base}/console/organization-${teamId}${url.search ?? ''}`); } else { - throw redirect(303, `${base}/console/onboarding`); + throw redirect(303, `${base}/console/onboarding${url.search ?? ''}`); } }; diff --git a/src/routes/console/account/+page.svelte b/src/routes/console/account/+page.svelte index 9fc3f66a3..6eb5ab19a 100644 --- a/src/routes/console/account/+page.svelte +++ b/src/routes/console/account/+page.svelte @@ -1,181 +1,14 @@ - - - Name - - -
      - -
    -
    - - - - -
    - -
    - - Email - - - - - {#if email !== $user.email && email} - - {/if} - - - - - - - -
    -
    - - Password -

    - Forgot your password? Recover your password -

    - - - - - - - - - - - -
    -
    - -
    - Delete account -
    -

    - Your account will be permanently deleted and access will be lost to any of your teams - and data. This action is irreversible. -

    - - - - - - -
    {$user.name}
    -
    -
    -
    - - - - -
    + + + +
    - - diff --git a/src/routes/console/account/delete.svelte b/src/routes/console/account/delete.svelte index 29e4f85b6..cfacb0543 100644 --- a/src/routes/console/account/delete.svelte +++ b/src/routes/console/account/delete.svelte @@ -11,7 +11,7 @@ async function deleteAccount() { try { - await sdk.forConsole.account.updateStatus(); + await sdk.forConsole.account.delete(); await invalidate(Dependencies.ACCOUNT); showDelete = false; addNotification({ diff --git a/src/routes/console/account/deleteAccount.svelte b/src/routes/console/account/deleteAccount.svelte new file mode 100644 index 000000000..3767b982d --- /dev/null +++ b/src/routes/console/account/deleteAccount.svelte @@ -0,0 +1,34 @@ + + + +
    + Delete account +
    +

    + Your account will be permanently deleted and access will be lost to any of your teams and + data. This action is irreversible. +

    + + + + + + +
    {$user.name}
    +
    +
    +
    + + + + +
    + + diff --git a/src/routes/console/account/header.svelte b/src/routes/console/account/header.svelte index 71d54f3d3..93fde74f9 100644 --- a/src/routes/console/account/header.svelte +++ b/src/routes/console/account/header.svelte @@ -9,10 +9,11 @@ import { Cover, CoverTitle } from '$lib/layout'; import { sdk } from '$lib/stores/sdk'; import { user } from '$lib/stores/user'; + import { isCloud } from '$lib/system'; const path = `/console/account`; - $: tabs = [ + $: permanentTabs = [ { href: path, title: 'Overview', @@ -37,6 +38,18 @@ } ]; + $: tabs = isCloud + ? [ + ...permanentTabs, + { + href: `${path}/payments`, + title: 'Payment details', + event: 'payments', + hasChildren: true + } + ] + : permanentTabs; + async function logout() { await sdk.forConsole.account.deleteSession('current'); trackEvent(Submit.AccountLogout); diff --git a/src/routes/console/account/organizations/+page.svelte b/src/routes/console/account/organizations/+page.svelte index f43f7c46c..a82d76a58 100644 --- a/src/routes/console/account/organizations/+page.svelte +++ b/src/routes/console/account/organizations/+page.svelte @@ -13,22 +13,43 @@ import CreateOrganization from '../../createOrganization.svelte'; import { sdk } from '$lib/stores/sdk'; import type { PageData } from './$types'; + import { isCloud } from '$lib/system'; + import { Pill } from '$lib/elements'; + import type { Models } from '@appwrite.io/console'; + import type { Organization } from '$lib/stores/organization'; + import { daysLeftInTrial } from '$lib/stores/billing'; + import { tooltip } from '$lib/actions/tooltip'; + import { toLocaleDate } from '$lib/helpers/date'; + import { wizard } from '$lib/stores/wizard'; + import CreateOrganizationCloud from '$routes/console/createOrganizationCloud.svelte'; + import { BillingPlan } from '$lib/constants'; export let data: PageData; + let addOrganization = false; const getMemberships = async (teamId: string) => { const memberships = await sdk.forConsole.teams.listMemberships(teamId); return memberships.memberships.map((team) => team.userName); }; - let addOrganization = false; + function isCloudOrg( + data: Partial> | Organization + ): data is Organization { + return isCloud && 'billingPlan' in data ? true : false; + } + + function createOrg() { + if (isCloud) { + wizard.start(CreateOrganizationCloud); + } else addOrganization = true; + }
    Organizations - @@ -39,7 +60,7 @@ total={data.organizations.total} offset={data.offset} event="organization" - on:click={() => (addOrganization = true)}> + on:click={createOrg}> {#each data.organizations.teams as organization} {@const avatarList = getMemberships(organization.$id)} @@ -49,6 +70,31 @@ {organization.name} + + {#if isCloudOrg(organization)} + {#if organization?.billingPlan === BillingPlan.STARTER} +
    + FREE +
    + {/if} + {#if organization?.billingTrialStartDate && $daysLeftInTrial > 0} +
    + TRIAL +
    + {/if} + {/if} +
    {#await avatarList} {:then avatars} @@ -61,7 +107,7 @@ {:else} - (addOrganization = true)}> +

    Create a new organization

    {/if} diff --git a/src/routes/console/account/payments/+page.svelte b/src/routes/console/account/payments/+page.svelte new file mode 100644 index 000000000..8cca7e6f8 --- /dev/null +++ b/src/routes/console/account/payments/+page.svelte @@ -0,0 +1,16 @@ + + + +
    + Payment details +
    + + +
    diff --git a/src/routes/console/account/payments/+page.ts b/src/routes/console/account/payments/+page.ts new file mode 100644 index 000000000..28fa52025 --- /dev/null +++ b/src/routes/console/account/payments/+page.ts @@ -0,0 +1,13 @@ +import { Dependencies } from '$lib/constants'; +import { sdk } from '$lib/stores/sdk'; +import type { PageLoad } from './$types'; + +export const load: PageLoad = async ({ parent, depends }) => { + await parent(); + depends(Dependencies.PAYMENT_METHODS); + depends(Dependencies.ADDRESS); + return { + paymentMethods: await sdk.forConsole.billing.listPaymentMethods(), + addressList: await sdk.forConsole.billing.listAddresses() + }; +}; diff --git a/src/routes/console/account/payments/addressModal.svelte b/src/routes/console/account/payments/addressModal.svelte new file mode 100644 index 000000000..fbeb783af --- /dev/null +++ b/src/routes/console/account/payments/addressModal.svelte @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/routes/console/account/payments/billingAddress.svelte b/src/routes/console/account/payments/billingAddress.svelte new file mode 100644 index 000000000..0440237a8 --- /dev/null +++ b/src/routes/console/account/payments/billingAddress.svelte @@ -0,0 +1,157 @@ + + + + Billing Address + +

    + View or update your billing address. This address will be included in your invoices from + Appwrite. +

    + + {#if $addressList.total && countryList?.total} + + + Billing address + + + + + {#each $addressList.billingAddresses as address, i} + {@const country = countryList?.countries?.find( + (c) => c.code === address.country + )} + {@const linkedOrgs = orgList?.filter( + (org) => address.$id === org.billingAddressId + )} + + + +
    +

    {address.streetAddress}

    + {#if address?.addressLine2} +

    {address.addressLine2}

    + {/if} +

    {address.city}

    +

    {address.state}

    +

    {address.postalCode}

    +

    {country ? country.name : address.country}

    +
    +
    + + {#if linkedOrgs?.length > 0} +
    +

    This billing address is linked to the following organizations:

    + ${linkedOrgs + .map( + (org) => + `${org.name}` + ) + .join('')} +
    ` + }}> + + linked to organization + + + {/if} +
    + + + + + { + showEdit = true; + selectedAddress = address; + showDropdown[i] = false; + }}> + Edit + + { + showDelete = true; + selectedAddress = address; + selectedLinkedOrgs = linkedOrgs; + showDropdown[i] = false; + }}> + Delete + + + + +
    + {/each} +
    +
    + + + {:else} + (show = true)}> +

    Add a billing address

    +
    + {/if} +
    +
    + + + + diff --git a/src/routes/console/account/payments/deleteAddressModal.svelte b/src/routes/console/account/payments/deleteAddressModal.svelte new file mode 100644 index 000000000..508ebc8fa --- /dev/null +++ b/src/routes/console/account/payments/deleteAddressModal.svelte @@ -0,0 +1,75 @@ + + + + + {linkedOrgs.length ? 'Unable to delete billing address' : 'Delete billing address'} + + + {#if linkedOrgs.length === 1} +

    + This billing address is set as the default for the {linkedOrgs[0].name}. As it has upcoming invoices it cannot be deleted from your account. +

    + {:else if linkedOrgs.length > 1} +

    + This billing address is set as the default for the following organisations. As they have + upcoming invoices it cannot be deleted from your account. +

    +
      + {#each linkedOrgs as org} +
    • + {org.name} +
    • + {/each} +
    + {:else} +

    Are you sure you want to delete this billing address from your account?

    + {/if} + + + + {#if !linkedOrgs.length} + + {/if} + +
    diff --git a/src/routes/console/account/payments/deletePaymentModal.svelte b/src/routes/console/account/payments/deletePaymentModal.svelte new file mode 100644 index 000000000..142df323c --- /dev/null +++ b/src/routes/console/account/payments/deletePaymentModal.svelte @@ -0,0 +1,74 @@ + + + + + {linkedOrgs.length ? 'Unable to delete payment method' : 'Delete payment method'} + + {#if linkedOrgs.length === 1} +

    + This payment method is set as the default for the {linkedOrgs[0].name}. As it has upcoming invoices it cannot be deleted from your account. +

    + {:else if linkedOrgs.length > 1} +

    + This payment method is set as the default for the following organisations. As they have + upcoming invoices it cannot be deleted from your account. +

    +
      + {#each linkedOrgs as org} +
    • + {org.name} +
    • + {/each} +
    + {:else} +

    Are you sure you want to delete this payment method from your account?

    + {/if} + + + + {#if !linkedOrgs.length} + + {/if} + +
    diff --git a/src/routes/console/account/payments/editAddressModal.svelte b/src/routes/console/account/payments/editAddressModal.svelte new file mode 100644 index 000000000..bea14ab0e --- /dev/null +++ b/src/routes/console/account/payments/editAddressModal.svelte @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/routes/console/account/payments/editPaymentModal.svelte b/src/routes/console/account/payments/editPaymentModal.svelte new file mode 100644 index 000000000..56529fdc9 --- /dev/null +++ b/src/routes/console/account/payments/editPaymentModal.svelte @@ -0,0 +1,107 @@ + + + + {#if selectedPaymentMethod?.expired} + + This payment method has expired + + {/if} + {#if isLinked} + Updates to this payment method will be applied to any linked organizations. + {/if} + + + + + + + + + + + diff --git a/src/routes/console/account/payments/paymentMethods.svelte b/src/routes/console/account/payments/paymentMethods.svelte new file mode 100644 index 000000000..e8a210ae6 --- /dev/null +++ b/src/routes/console/account/payments/paymentMethods.svelte @@ -0,0 +1,158 @@ + + + + Payment Methods + +

    + View or update your payment methods. These can be applied to any organizations you have + created. +

    + + {#if $paymentMethods?.total && filteredMethods?.length > 0} + + + Payment methods + + + {#each filteredMethods as paymentMethod, i} + {@const linkedOrgs = orgList?.filter( + (org) => + paymentMethod.$id === org.paymentMethodId || + paymentMethod.$id === org.backupPaymentMethodId + )} + + + + +
    + {#if linkedOrgs?.length > 0} +
    +

    This payment method is linked to the following organizations:

    + ${linkedOrgs + .map( + (org) => + `${org.name}` + ) + .join('')} +
    ` + }}> + + linked to organization + +
    + {/if} + + + + { + showEdit = true; + showDropdown[i] = false; + selectedMethod = paymentMethod; + isLinked = !!linkedOrgs?.length; + }}> + Edit + + { + selectedMethod = paymentMethod; + selectedLinkedOrgs = linkedOrgs; + showDelete = true; + showDropdown[i] = false; + }}> + Delete + + + + +
    +
    +
    + {/each} +
    +
    + + {:else} + (showPayment = true)}> +

    Add a payment method

    +
    + {/if} +
    +
    + +{#if showPayment && isCloud && hasStripePublicKey} + +{/if} +{#if showEdit && isCloud && hasStripePublicKey} + +{/if} + +{#if selectedMethod} + +{/if} diff --git a/src/routes/console/account/payments/paymentModal.svelte b/src/routes/console/account/payments/paymentModal.svelte new file mode 100644 index 000000000..abe6767fe --- /dev/null +++ b/src/routes/console/account/payments/paymentModal.svelte @@ -0,0 +1,107 @@ + + + + + +
    +
    +
    +
    +
    + +
    +
    +
    + + + + +
    + + diff --git a/src/routes/console/account/updateEmail.svelte b/src/routes/console/account/updateEmail.svelte new file mode 100644 index 000000000..d929e4e35 --- /dev/null +++ b/src/routes/console/account/updateEmail.svelte @@ -0,0 +1,62 @@ + + +
    + + Email + + + + + {#if email !== $user.email && email} + + {/if} + + + + + + + +
    diff --git a/src/routes/console/account/updateName.svelte b/src/routes/console/account/updateName.svelte new file mode 100644 index 000000000..98ebc498f --- /dev/null +++ b/src/routes/console/account/updateName.svelte @@ -0,0 +1,51 @@ + + +
    + + Name + + + + + + + + + + + +
    diff --git a/src/routes/console/account/updatePassword.svelte b/src/routes/console/account/updatePassword.svelte new file mode 100644 index 000000000..668b5dbdd --- /dev/null +++ b/src/routes/console/account/updatePassword.svelte @@ -0,0 +1,59 @@ + + +
    + + Password +

    + Forgot your password? Recover your password +

    + + + + + + + + + + + +
    +
    diff --git a/src/routes/console/changeOrganizationTierCloud.svelte b/src/routes/console/changeOrganizationTierCloud.svelte new file mode 100644 index 000000000..a76e16e6d --- /dev/null +++ b/src/routes/console/changeOrganizationTierCloud.svelte @@ -0,0 +1,208 @@ + + + diff --git a/src/routes/console/createOrganizationCloud.svelte b/src/routes/console/createOrganizationCloud.svelte new file mode 100644 index 000000000..518c61f9f --- /dev/null +++ b/src/routes/console/createOrganizationCloud.svelte @@ -0,0 +1,146 @@ + + + diff --git a/src/routes/console/feedbackWizard.svelte b/src/routes/console/feedbackWizard.svelte index fd55199c3..9a7553ae1 100644 --- a/src/routes/console/feedbackWizard.svelte +++ b/src/routes/console/feedbackWizard.svelte @@ -3,8 +3,8 @@ import { onDestroy } from 'svelte'; import type { WizardStepsType } from '$lib/layout/wizard.svelte'; import { feedbackData } from '$lib/stores/feedback'; - import Step1 from './wizardFeedback/step1.svelte'; - import Step2 from './wizardFeedback/step2.svelte'; + import Step1 from './wizard/feedback/step1.svelte'; + import Step2 from './wizard/feedback/step2.svelte'; onDestroy(() => { feedbackData.reset(); diff --git a/src/routes/console/onboarding/+page.svelte b/src/routes/console/onboarding/+page.svelte index 8f45cc92c..8785fb04e 100644 --- a/src/routes/console/onboarding/+page.svelte +++ b/src/routes/console/onboarding/+page.svelte @@ -1,73 +1,159 @@ -
    - + Create a new organization + + {#if !showCustomId}
    (showCustomId = !showCustomId)}>
    {:else} - + {/if} - . +

    + + + +
    + {/if} + diff --git a/src/routes/console/onboarding/header.svelte b/src/routes/console/onboarding/header.svelte index 51f7314c6..4bf595ce1 100644 --- a/src/routes/console/onboarding/header.svelte +++ b/src/routes/console/onboarding/header.svelte @@ -1,7 +1,10 @@ - Let's create your first project +
    + Welcome to Appwrite +
    diff --git a/src/routes/console/organization-[organization]/+layout.svelte b/src/routes/console/organization-[organization]/+layout.svelte index dbc4fbb26..77dcfa320 100644 --- a/src/routes/console/organization-[organization]/+layout.svelte +++ b/src/routes/console/organization-[organization]/+layout.svelte @@ -1,12 +1,12 @@ - -
    - Projects - - - - (showCreate = true)}>Empty project - -
    - Import project Experimental -
    -
    -
    -
    -
    +{#if $organization?.$id} + +
    + Projects - {#if data.projects.total} - (showCreate = true)}> - {#each data.projects.projects as project} -
  • - - - {project?.platforms?.length ? project?.platforms?.length : 'No'} apps - - - {project.name} - - {#if allServiceDisabled(project)} -

    -

    - {/if} - {@const platforms = filterPlatforms( - project.platforms.map((platform) => getPlatformInfo(platform.type)) - )} - {#each platforms as platform, i} - {#if i < 3} + + + + (showCreate = true)}>Empty project + +
    + Import project Experimental +
    +
    +
    +
    +
  • + + {#if data.projects.total} + + {#each data.projects.projects as project} + {@const platforms = filterPlatforms( + project.platforms.map((platform) => getPlatformInfo(platform.type)) + )} +
  • + + + {project?.platforms?.length ? project?.platforms?.length : 'No'} apps + + + {project.name} + + {#if allServiceDisabled(project)} +

    +

    + {/if} + + {#each platforms as platform, i} + {#if i < 3} + + + {/if} + {/each} + {#if platforms?.length > 3} - {/if} - {/each} - {#if platforms?.length > 3} - - +{project.platforms.length - 3} - - {/if} -
    -
  • - {/each} - -

    Create a new project

    -
    -
    - {:else} - (showCreate = true)}> -

    Create a new project

    -
    - {/if} + + {#if isCloud && regions} + {@const region = findRegion(project)} + + {region?.name} + + {/if} + + + + {/each} + +

    Create a new project

    +
    + + {:else} + + {/if} - -
    + +
    - - + + +{/if} diff --git a/src/routes/console/organization-[organization]/+page.ts b/src/routes/console/organization-[organization]/+page.ts index fab93a7b4..ff2108331 100644 --- a/src/routes/console/organization-[organization]/+page.ts +++ b/src/routes/console/organization-[organization]/+page.ts @@ -1,10 +1,12 @@ import { Query } from '@appwrite.io/console'; import { sdk } from '$lib/stores/sdk'; import { getLimit, getPage, pageToOffset } from '$lib/helpers/load'; -import { CARD_LIMIT } from '$lib/constants'; +import { CARD_LIMIT, Dependencies } from '$lib/constants'; import type { PageLoad } from './$types'; -export const load: PageLoad = async ({ params, url, route }) => { +export const load: PageLoad = async ({ params, url, route, depends, parent }) => { + await parent(); + depends(Dependencies.ORGANIZATION); const page = getPage(url); const limit = getLimit(url, route, CARD_LIMIT); const offset = pageToOffset(page, limit); @@ -15,7 +17,7 @@ export const load: PageLoad = async ({ params, url, route }) => { projects: await sdk.forConsole.projects.list([ Query.offset(offset), Query.equal('teamId', params.organization), - Query.limit(CARD_LIMIT), + Query.limit(limit), Query.orderDesc('') ]) }; diff --git a/src/routes/console/organization-[organization]/billing/+page.svelte b/src/routes/console/organization-[organization]/billing/+page.svelte new file mode 100644 index 000000000..a6055bedb --- /dev/null +++ b/src/routes/console/organization-[organization]/billing/+page.svelte @@ -0,0 +1,103 @@ + + + + {#if $failedInvoice} + + + The scheduled payment for {$organization.name} failed + + To avoid service disruptions in organization's your projects, please verify your payment + details and update them if necessary. + + {/if} + {#if defaultPaymentMethod?.failed && !backupPaymentMethod} + + + The default payment method for {$organization.name} has expired + + To avoid service disruptions in your organization's projects, please update your payment + details. + + {/if} + {#if $organization?.billingPlanDowngrade} + + Your organization will change to a Starter plan once your current billing cycle ends on {toLocaleDate( + $organization.billingNextInvoiceDate + )}. + + {/if} +
    + Billing +
    + + + + + + + {#if $organization?.billingPlan !== BillingPlan.STARTER && !!$organization?.billingBudget} + + {/if} + +
    diff --git a/src/routes/console/organization-[organization]/billing/+page.ts b/src/routes/console/organization-[organization]/billing/+page.ts new file mode 100644 index 000000000..a1eb3b685 --- /dev/null +++ b/src/routes/console/organization-[organization]/billing/+page.ts @@ -0,0 +1,18 @@ +import { Dependencies } from '$lib/constants'; +import { sdk } from '$lib/stores/sdk'; +import type { PageLoad } from './$types'; + +export const load: PageLoad = async ({ parent, depends }) => { + const { organization } = await parent(); + depends(Dependencies.PAYMENT_METHODS); + depends(Dependencies.ORGANIZATION); + depends(Dependencies.CREDIT); + depends(Dependencies.INVOICES); + depends(Dependencies.ADDRESS); + + return { + paymentMethods: await sdk.forConsole.billing.listPaymentMethods(), + addressList: await sdk.forConsole.billing.listAddresses(), + aggregationList: await sdk.forConsole.billing.listAggregation(organization.$id) + }; +}; diff --git a/src/routes/console/organization-[organization]/billing/addCreditModal.svelte b/src/routes/console/organization-[organization]/billing/addCreditModal.svelte new file mode 100644 index 000000000..f0bb10ba9 --- /dev/null +++ b/src/routes/console/organization-[organization]/billing/addCreditModal.svelte @@ -0,0 +1,50 @@ + + + + Apply Appwrite credits to your organization. + + + + + + + + + + diff --git a/src/routes/console/organization-[organization]/billing/addCreditWizard.svelte b/src/routes/console/organization-[organization]/billing/addCreditWizard.svelte new file mode 100644 index 000000000..5b4029d34 --- /dev/null +++ b/src/routes/console/organization-[organization]/billing/addCreditWizard.svelte @@ -0,0 +1,68 @@ + + + diff --git a/src/routes/console/organization-[organization]/billing/availableCredit.svelte b/src/routes/console/organization-[organization]/billing/availableCredit.svelte new file mode 100644 index 000000000..753e389d7 --- /dev/null +++ b/src/routes/console/organization-[organization]/billing/availableCredit.svelte @@ -0,0 +1,117 @@ + + + + Available credit + +

    Appwrite credit will automatically be applied to your next invoice.

    + +
    +

    + Credit balance ${balance} +

    + {#if creditList?.total} + + {/if} +
    + {#if creditList?.total} + + + Date Added + Expiry Date + Amount + + + {#each creditList.credits as credit} + + + {toLocaleDate(credit.$createdAt)} + + + {toLocaleDate(credit.expiration)} + + + ${credit.credits} + + + {/each} + +
    +
    +

    Total results: {creditList?.total}

    + +
    + {:else} + Add credits + {/if} +
    +
    + + diff --git a/src/routes/console/organization-[organization]/billing/billingAddress.svelte b/src/routes/console/organization-[organization]/billing/billingAddress.svelte new file mode 100644 index 000000000..aa432851b --- /dev/null +++ b/src/routes/console/organization-[organization]/billing/billingAddress.svelte @@ -0,0 +1,166 @@ + + + + Billing address + +

    + View or update your billing address. This address will be included in your invoices from + Appwrite. +

    + + {#if $organization?.billingAddressId && billingAddress} +
    +
    +
    +

    {billingAddress.streetAddress}

    + {#if billingAddress?.addressLine2} +

    {billingAddress.addressLine2}

    + {/if} +

    {billingAddress.city}

    +

    {billingAddress.state}

    +

    {billingAddress.postalCode}

    +

    {billingAddress.country}

    +
    + + + + + { + showEdit = true; + showBillingAddressDropdown = false; + }}> + Edit + + { + showReplace = true; + showBillingAddressDropdown = false; + }}> + Replace + + + +
    +
    + {:else} +
    +
    +
    + + + + {#if $addressList?.total} + {#each $addressList.billingAddresses as address} + addAddress(address?.$id)}> +

    + {address.streetAddress}, + {#if address?.addressLine2} + {address.addressLine2}, + {/if} + {address.city}, + {address.state}, + {address.postalCode}, + {address.country} +

    +
    + {/each} + {/if} + { + showCreate = true; + showDropdown = false; + }}> + Add new billing address + +
    +
    +
    +
    + Add a billing address +
    +
    +
    + {/if} +
    +
    + +{#if showCreate} + +{/if} +{#if showEdit} + +{/if} +{#if showReplace} + +{/if} diff --git a/src/routes/console/organization-[organization]/billing/budgetAlert.svelte b/src/routes/console/organization-[organization]/billing/budgetAlert.svelte new file mode 100644 index 000000000..a75439aac --- /dev/null +++ b/src/routes/console/organization-[organization]/billing/budgetAlert.svelte @@ -0,0 +1,142 @@ + + +
    + + Budget alerts + +

    + Get notified by email when your organization reaches or exceeds a percent of your + specified budget cap. You can set a maximum of 3 alerts. +

    + + +
    + (search = selectedAlert.toString())} /> +
    + +
    +
    +
    + + {#if alerts.length} + + + Alert at budget cap % + + + + {#each alerts.sort() as alert} + + + {alert}% + + + + + + {/each} + +
    + {/if} +
    + + + + +
    +
    diff --git a/src/routes/console/organization-[organization]/billing/budgetCap.svelte b/src/routes/console/organization-[organization]/billing/budgetCap.svelte new file mode 100644 index 000000000..137c70ba4 --- /dev/null +++ b/src/routes/console/organization-[organization]/billing/budgetCap.svelte @@ -0,0 +1,98 @@ + + +
    + + Budget cap + +

    + Restrict your resource usage by setting a budget cap. +

    + + {#if $organization?.billingPlan === BillingPlan.STARTER} + + + Budget caps are a Pro plan feature + + Upgrade to a Pro plan to set a budget cap for your organization. For more information + on what you can do with a Pro plan, + view our pricing guide. + + {:else} + + + + Budget cap limits do not include the base amount of your plan. Cap usage + is reset at the beginning of each billing cycle. + + + {#if capActive} + + {/if} + + {/if} + + + + + +
    +
    diff --git a/src/routes/console/organization-[organization]/billing/deleteOrgPayment.svelte b/src/routes/console/organization-[organization]/billing/deleteOrgPayment.svelte new file mode 100644 index 000000000..747b538ec --- /dev/null +++ b/src/routes/console/organization-[organization]/billing/deleteOrgPayment.svelte @@ -0,0 +1,88 @@ + + +{#if disabled} + +

    + The {isBackup ? 'backup' : 'default'} payment method cannot be deleted as + {$organization?.name} + has an upcoming invoice. To proceed with deletion, set a {isBackup + ? 'default' + : 'backup'} or add a new {isBackup ? 'backup' : 'default'} payment method. +

    + + + +
    +{:else} + +

    + Are you sure you want to delete the payment method from {$organization?.name}? +

    + + + + +
    +{/if} diff --git a/src/routes/console/organization-[organization]/billing/paymentHistory.svelte b/src/routes/console/organization-[organization]/billing/paymentHistory.svelte new file mode 100644 index 000000000..d0b628c72 --- /dev/null +++ b/src/routes/console/organization-[organization]/billing/paymentHistory.svelte @@ -0,0 +1,146 @@ + + + + Payment history + +

    + Transaction history for this organization. Download invoices for more details about your + payments. +

    + + {#if invoiceList.total - 1 > 0} + + + Due Date + Status + Amount Due + + + + {#each invoiceList?.invoices as invoice, i} + {@const status = invoice.status} + {#if i !== 0} + + + {toLocaleDate(invoice.dueAt)} + + + + + {status === 'requires_authentication' ? 'failed' : status} + + + ${invoice.amount} + + + + + + (showDropdown[i] = !showDropdown[i])} + event="view_invoice"> + View invoice + + { + showDropdown[i] = !showDropdown[i]; + }} + event="download_invoice"> + Download PDF + + + + + + {/if} + {/each} + + +
    +

    Total results: {invoiceList?.total - 1 ?? 0}

    + +
    + {:else} + +

    + You have no payment history. After you receive your first invoice, you'll see it + here. +

    +
    + {/if} +
    +
    diff --git a/src/routes/console/organization-[organization]/billing/paymentMethods.svelte b/src/routes/console/organization-[organization]/billing/paymentMethods.svelte new file mode 100644 index 000000000..ae44de05e --- /dev/null +++ b/src/routes/console/organization-[organization]/billing/paymentMethods.svelte @@ -0,0 +1,329 @@ + + + + Payment methods + +

    View or update your organization payment methods here.

    + +
    +

    Default

    + {#if $organization?.paymentMethodId} + + + + + { + isSelectedBackup = false; + showEdit = true; + showDropdown = false; + }}> + Edit + + { + isSelectedBackup = false; + showReplace = true; + showDropdown = false; + }}> + Replace + + { + isSelectedBackup = false; + showDelete = true; + showDropdown = false; + }}> + Delete + + + + + {:else} + {@const filteredPaymentMethods = $paymentMethods.paymentMethods.filter( + (o) => !!o.last4 && o.$id !== $organization?.backupPaymentMethodId + )} +
    +
    +
    + + + + {#if $paymentMethods.total} + {#each filteredPaymentMethods as paymentMethod} + { + showDropdown = false; + addPaymentMethod(paymentMethod?.$id); + }}> + +

    + Card ending in {paymentMethod.last4} +

    + +
    +
    + {/each} + {/if} + (showPayment = true)}> + Add new payment method + +
    +
    +
    +
    + Add a payment method +
    +
    +
    + {/if} +
    +
    +

    Backup

    + {#if $organization?.backupPaymentMethodId} + + + + + { + showEdit = true; + isSelectedBackup = true; + showDropdownBackup = false; + }}> + Edit + + { + showReplace = true; + isSelectedBackup = true; + showDropdownBackup = false; + }}> + Replace + + { + showDelete = true; + isSelectedBackup = true; + showDropdownBackup = false; + }}> + Delete + + + + + {:else} + {@const filteredPaymentMethods = $paymentMethods.paymentMethods.filter( + (o) => !!o.last4 && o.$id !== $organization?.paymentMethodId + )} +
    +
    +
    + + + + {#if $paymentMethods.total} + {#each filteredPaymentMethods as paymentMethod} + { + showDropdownBackup = true; + addBackupPaymentMethod(paymentMethod?.$id); + }}> + +

    + Card ending in {paymentMethod.last4} +

    + +
    +
    + {/each} + {/if} + (showPayment = true)}> + Add new payment method + +
    +
    +
    +
    + Add a backup payment method + +
    +
    +
    + {/if} +
    +
    +
    + +{#if showPayment && isCloud && hasStripePublicKey} + { + if (isSelectedBackup) { + addBackupPaymentMethod(e.detail.$id); + } else { + addPaymentMethod(e.detail.$id); + } + }} /> +{/if} +{#if showEdit && isCloud && hasStripePublicKey} + +{/if} +{#if isCloud && hasStripePublicKey} + +{/if} +{#if showDelete && isCloud && hasStripePublicKey} + +{/if} diff --git a/src/routes/console/organization-[organization]/billing/planSummary.svelte b/src/routes/console/organization-[organization]/billing/planSummary.svelte new file mode 100644 index 000000000..746ae32c1 --- /dev/null +++ b/src/routes/console/organization-[organization]/billing/planSummary.svelte @@ -0,0 +1,160 @@ + + +{#if $organization} + + Overview + +

    + A summary of the current plan. For more information on Appwrite plans, + view our pricing page. + +

    + + +
    +
    +
    + {tierToPlan($organization?.billingPlan)?.name} plan +
    + {#if $organization?.billingPlan !== BillingPlan.STARTER && isTrial} + TRIAL + {/if} +
    + +

    + {#if !extraUsage} + Total to-date: + {/if} + + ${isTrial ? 0 : currentPlan?.price} + +

    +
    + {#if currentInvoice?.usage?.length && $organization?.billingPlan !== BillingPlan.STARTER && !isTrial} + {@const extraMembers = currentInvoice.usage.find((u) => u.name === 'members')} + {#if extraMembers} +
    + Additional members +
      +
    • +

      + {extraMembers.value} + {extraMembers.name} +

      +

      ${extraMembers.amount}

      +
    • +
    +
    + {/if} +
    + Addons +
      + {#each currentInvoice.usage as excess} + {#if ['storage', 'bandwidth'].includes(excess.name)} + {@const excessValue = humanFileSize(excess.value)} +
    • +

      + + {excessValue.value}{excessValue.unit} + + {excess.name} +

      +

      ${excess.amount}

      +
    • + {/if} + {#if ['users', 'executions'].includes(excess.name)} +
    • +

      + {abbreviateNumber(excess.value)} + {excess.name} +

      +

      ${excess.amount}

      +
    • + {/if} + {/each} +
    • +

      Total to-date:

      +

      ${currentInvoice?.amount}

      +
    • +
    +
    + {/if} +
    +
    +

    Billing period:

    +

    + {toLocaleDate($organization?.billingCurrentInvoiceDate)} - {toLocaleDate( + $organization?.billingNextInvoiceDate + )} +

    +
    +
    + + {#if $organization?.billingPlan === BillingPlan.STARTER} +
    + + +
    + {:else} +
    + + +
    + {/if} +
    +
    +{/if} diff --git a/src/routes/console/organization-[organization]/billing/replaceAddress.svelte b/src/routes/console/organization-[organization]/billing/replaceAddress.svelte new file mode 100644 index 000000000..0eb06bbef --- /dev/null +++ b/src/routes/console/organization-[organization]/billing/replaceAddress.svelte @@ -0,0 +1,195 @@ + + + +

    Replace the existing billing address for your organization.

    + {#if addresses?.total} + + + +
    +
    +

    {element.streetAddress}

    + {#if element?.addressLine2} +

    {element.addressLine2}

    + {/if} +

    {element.city}

    +

    {element.state}

    +

    {element.postalCode}

    +

    {element.country}

    +
    + {#if $organization.billingAddressId === element.$id} + Current + {/if} +
    +
    + + Add a new billing address + + + + + + + + + + + +
    +
    + {/if} + + + + +
    diff --git a/src/routes/console/organization-[organization]/billing/replaceCard.svelte b/src/routes/console/organization-[organization]/billing/replaceCard.svelte new file mode 100644 index 000000000..8dfc4fe50 --- /dev/null +++ b/src/routes/console/organization-[organization]/billing/replaceCard.svelte @@ -0,0 +1,222 @@ + + + +

    Replace the existing payment method for your organization.

    + +
    + {#if methods?.total} + {#each filteredMethods as method} +
    + + + + + {method.brand} ending in {method.last4} + + + {#if method.$id === $organization.backupPaymentMethodId} + Backup + {:else if method.$id === $organization.paymentMethodId} + Default + {/if} + + +
    + {/each} + {/if} + +
    + {#if methods?.total} + + Add new payment method + + {/if} + {#if selectedPaymentMethodId === null} + + +
    +
    +
    +
    +
    + +
    +
    +
    + {/if} +
    +
    +
    + + + + +
    + + diff --git a/src/routes/console/organization-[organization]/billing/store.ts b/src/routes/console/organization-[organization]/billing/store.ts new file mode 100644 index 000000000..e75622d62 --- /dev/null +++ b/src/routes/console/organization-[organization]/billing/store.ts @@ -0,0 +1,15 @@ +import { page } from '$app/stores'; +import type { WizardStepsType } from '$lib/layout/wizard.svelte'; +import type { AggregationList } from '$lib/sdk/billing'; +import { derived, writable } from 'svelte/store'; + +export const aggregationList = derived( + page, + ($page) => $page.data.aggregationList as AggregationList +); + +export const addCreditWizardSteps = writable(new Map()); +export const addCreditWizardStore = writable<{ coupon: string; paymentMethodId: string }>({ + coupon: null, + paymentMethodId: null +}); diff --git a/src/routes/console/organization-[organization]/billing/taxId.svelte b/src/routes/console/organization-[organization]/billing/taxId.svelte new file mode 100644 index 000000000..003012cbb --- /dev/null +++ b/src/routes/console/organization-[organization]/billing/taxId.svelte @@ -0,0 +1,55 @@ + + +
    + + Tax ID + Add a tax identification number to your organization. + + + + + + + + + + +
    diff --git a/src/routes/console/organization-[organization]/billing/wizard/addCredit.svelte b/src/routes/console/organization-[organization]/billing/wizard/addCredit.svelte new file mode 100644 index 000000000..3e4a3707b --- /dev/null +++ b/src/routes/console/organization-[organization]/billing/wizard/addCredit.svelte @@ -0,0 +1,46 @@ + + + + Add credits + + Add Appwrite credits to your organization. A payment method is required before credits can + be applied. + + { + $addCreditWizardStore.coupon = e.detail.code; + }} + let:data> + {data.code} has been successfully added + + diff --git a/src/routes/console/organization-[organization]/billing/wizard/paymentDetails.svelte b/src/routes/console/organization-[organization]/billing/wizard/paymentDetails.svelte new file mode 100644 index 000000000..b4f54264d --- /dev/null +++ b/src/routes/console/organization-[organization]/billing/wizard/paymentDetails.svelte @@ -0,0 +1,57 @@ + + + + Payment details + + Add a payment method to your organization. You will not be charged until you upgrade to a + Pro or Scale plan. + + +

    Payment method

    + + + +
    diff --git a/src/routes/console/organization-[organization]/breadcrumbs.svelte b/src/routes/console/organization-[organization]/breadcrumbs.svelte index 62b3a6f20..e2a847beb 100644 --- a/src/routes/console/organization-[organization]/breadcrumbs.svelte +++ b/src/routes/console/organization-[organization]/breadcrumbs.svelte @@ -4,8 +4,8 @@ $: breadcrumbs = [ { - href: `/console/organization-${$organization.$id}`, - title: $organization.name + href: `/console/organization-${$organization?.$id}`, + title: $organization?.name } ]; diff --git a/src/routes/console/organization-[organization]/createMember.svelte b/src/routes/console/organization-[organization]/createMember.svelte index 669d7fafa..5bd7742ba 100644 --- a/src/routes/console/organization-[organization]/createMember.svelte +++ b/src/routes/console/organization-[organization]/createMember.svelte @@ -1,34 +1,41 @@ + {#if isCloud} + + {#if $organization?.billingPlan === BillingPlan.SCALE} + You can add unlimited organization members on the {plan.name} plan at no cost. + {:else if $organization?.billingPlan === BillingPlan.PRO} + You can add unlimited organization members on the {plan.name} plan for + ${plan.addons.member.price} each per billing period. + {/if} + + {/if} + import { Wizard } from '$lib/layout'; + import { sdk } from '$lib/stores/sdk'; + import { onDestroy } from 'svelte'; + import { addNotification } from '$lib/stores/notifications'; + import Step1 from './wizard/step1.svelte'; + import Step2 from './wizard/step2.svelte'; + import type { WizardStepsType } from '$lib/layout/wizard.svelte'; + import { goto, invalidate } from '$app/navigation'; + import { Dependencies } from '$lib/constants'; + import { page } from '$app/stores'; + import { Submit, trackEvent, trackError } from '$lib/actions/analytics'; + import { ID } from '@appwrite.io/console'; + import { createProject } from './wizard/store'; + import { wizard } from '$lib/stores/wizard'; + + const teamId = $page.params.organization; + + async function onFinish() { + await invalidate(Dependencies.FUNCTIONS); + } + + async function create() { + try { + const project = await sdk.forConsole.projects.create( + $createProject?.id ?? ID.unique(), + $createProject.name, + teamId, + $createProject.region + ); + trackEvent(Submit.ProjectCreate, { + customId: !!$createProject?.id, + teamId, + region: $createProject.region + }); + addNotification({ + type: 'success', + message: `${$createProject.name} has been created` + }); + await goto(`/console/project-${project.$id}`); + wizard.hide(); + } catch (e) { + addNotification({ + type: 'error', + message: e.message + }); + trackError(e, Submit.ProjectCreate); + } + } + + onDestroy(() => { + $createProject = { + id: null, + name: null, + region: 'fra' + }; + }); + + const stepsComponents: WizardStepsType = new Map(); + stepsComponents.set(1, { + label: 'Details', + component: Step1 + }); + stepsComponents.set(2, { + label: 'Region', + component: Step2 + }); + + + diff --git a/src/routes/console/organization-[organization]/deleteMember.svelte b/src/routes/console/organization-[organization]/deleteMember.svelte index 7c12dd1d1..5a6cd2687 100644 --- a/src/routes/console/organization-[organization]/deleteMember.svelte +++ b/src/routes/console/organization-[organization]/deleteMember.svelte @@ -1,6 +1,6 @@ - - -

    - Are you sure you want to delete {$organization.name}? All projects ({$organization.total}) - and data associated with this organization will be deleted. This action is irreversible. -

    - - - - -
    diff --git a/src/routes/console/organization-[organization]/excesLimitModal.svelte b/src/routes/console/organization-[organization]/excesLimitModal.svelte new file mode 100644 index 000000000..b3a58ac1c --- /dev/null +++ b/src/routes/console/organization-[organization]/excesLimitModal.svelte @@ -0,0 +1,102 @@ + + + + + Your usage exceeds the {tierToPlan($organization.billingPlan).name} plan limits + + + Appwrite Pro is now available. To facilitate a smooth transition for your projects, Starter plan + will maintain its current state of unlimited resource usage. This extension will be in effect + until January 31st, 2024. + + {#if $organization.billingPlan === BillingPlan.STARTER} +

    + Usage for {$organization.name} organization has reached the limits of the {tierToPlan( + $organization.billingPlan + ).name} plan. Consider upgrading to increase your resource usage. +

    + {:else} +

    + Usage for {$organization.name} organization has reached the limits of the {tierToPlan( + $organization.billingPlan + ).name} plan. Excess usage fees will apply. +

    + {/if} + + + +
    + +
    + + + +
    +
    +
    +
    diff --git a/src/routes/console/organization-[organization]/header.svelte b/src/routes/console/organization-[organization]/header.svelte index a206639f2..23c2d23d8 100644 --- a/src/routes/console/organization-[organization]/header.svelte +++ b/src/routes/console/organization-[organization]/header.svelte @@ -1,10 +1,15 @@ - - - - - - {#each $organizationList.teams as org} - (showDropdown = false)}> - {org.name} - - {/each} - - -
    -
      - newOrgModal.set(true)}> - New organization - -
    -
    -
    -
    -
    - - - - -
    -
    -
    - - {#each tabs as tab} - - {tab.title} - - {/each} - -
    +{#if $organization?.$id} + + + + + + {#each $organizationList.teams as org} + (showDropdown = false)}> + {org.name} + + {/each} + + +
    +
      + + New Organization + +
    +
    +
    +
    +
    + + + +
    + +
    +
    +
    + + {#each tabs as tab} + + {tab.title} + + {/each} + +
    +{/if} diff --git a/src/routes/console/organization-[organization]/members/+page.svelte b/src/routes/console/organization-[organization]/members/+page.svelte index 140a59510..8497dac36 100644 --- a/src/routes/console/organization-[organization]/members/+page.svelte +++ b/src/routes/console/organization-[organization]/members/+page.svelte @@ -1,9 +1,7 @@ + + + {#if upcomingInvoice} +

    + The organization {$organization.name} will be flagged for deletion. +

    + +

    + All existing projects will be paused and the organization will be deleted once your + upcoming invoice is processed on {toLocaleDate($organization.billingNextInvoiceDate)}. +

    + {:else} +

    + Are you sure you want to delete {$organization.name}? All projects ({$projects.total}) + and data associated with this organization will be deleted. This action is irreversible. +

    + {/if} + + + + +
    diff --git a/src/routes/console/organization-[organization]/settings/downloadDPA.svelte b/src/routes/console/organization-[organization]/settings/downloadDPA.svelte new file mode 100644 index 000000000..868dc3b54 --- /dev/null +++ b/src/routes/console/organization-[organization]/settings/downloadDPA.svelte @@ -0,0 +1,54 @@ + + + +
    + Download DPA document +
    +

    + After downloading, have the DPA signed by your organization's compliance authority, such as + your CEO or Compliance Manager, and submit it to privacy@appwrite.io. +

    + + +
    + Data Processing Agreement (DPA) document +
    +

    + The DPA is a legal document that describes the roles and responsibilities of + Appwrite and the organization when personal data is processed. Learn more about the DPA. +

    + +
    +
    +
    diff --git a/src/routes/console/organization-[organization]/store.ts b/src/routes/console/organization-[organization]/store.ts new file mode 100644 index 000000000..07b0ca110 --- /dev/null +++ b/src/routes/console/organization-[organization]/store.ts @@ -0,0 +1,6 @@ +import { page } from '$app/stores'; +import type { Models } from '@appwrite.io/console'; +import { derived, writable } from 'svelte/store'; + +export const showExcess = writable(false); +export const projects = derived(page, ($page) => $page.data?.projects as Models.ProjectList); diff --git a/src/routes/console/organization-[organization]/usage/[[invoice]]/+page.svelte b/src/routes/console/organization-[organization]/usage/[[invoice]]/+page.svelte new file mode 100644 index 000000000..475e23ed5 --- /dev/null +++ b/src/routes/console/organization-[organization]/usage/[[invoice]]/+page.svelte @@ -0,0 +1,296 @@ + + + +
    + Usage + + {#if $organization?.billingPlan === BillingPlan.STARTER} + + {/if} +
    +
    + {#if $organization.billingPlan === BillingPlan.SCALE} +

    + On the Scale plan, you'll be charged only for any usage that exceeds the thresholds + per resource listed below. +

    + {:else if $organization.billingPlan === BillingPlan.PRO} +

    + On the Pro plan, you'll be charged only for any usage that exceeds the thresholds + per resource listed below. +

    + {:else if $organization.billingPlan === BillingPlan.STARTER} +

    + If you exceed the limits of the {plan} plan, services for your organization's projects + may be disrupted. + . +

    + {/if} + + +
    + + + Bandwidth + +

    + Calculated for all bandwidth used across all projects in your organization. +

    + + + {#if data.organizationUsage.bandwidth} + {@const current = total(data.organizationUsage.bandwidth)} + {@const currentHumanized = humanFileSize(current)} + {@const max = getServiceLimit('bandwidth', tier)} + + + value + ? `${humanFileSize(+value).value} ${ + humanFileSize(+value).unit + }` + : '0' + } + } + }} + series={[ + { + name: 'Bandwidth', + data: [ + ...data.organizationUsage.bandwidth.map((e) => [e.date, e.value]) + ], + tooltip: { + valueFormatter: (value) => + `${humanFileSize(+value).value} ${humanFileSize(+value).unit}` + } + } + ]} /> + + {:else} + +
    +
    +
    + {/if} +
    +
    + + + Users + +

    The total number of users across all projects in your organization.

    + + + {#if data.organizationUsage.users} + {@const current = data.organizationUsage.usersTotal} + {@const max = getServiceLimit('users', tier)} + + + + {:else} + +
    +
    +
    + {/if} +
    +
    + + + Executions + +

    + Calculated for all functions that are executed in all projects in your organization. +

    + + + {#if data.organizationUsage.executionsTotal} + {@const current = data.organizationUsage.executionsTotal} + {@const max = getServiceLimit('executions', tier)} + + [e.date, e.value]) + ] + } + ]} /> + + {:else} + +
    +
    +
    + {/if} +
    +
    + + + Storage + +

    + Calculated for all your files, deployments, builds and databases. While in beta, only + file storage is counted against your plan limits. +

    + + + {#if data.organizationUsage.storageTotal} + {@const current = data.organizationUsage.storageTotal} + {@const currentHumanized = humanFileSize(current)} + {@const max = getServiceLimit('storage', tier)} + + + {:else} + +
    +
    +
    + {/if} +
    +
    + +

    + Metrics are estimates updated every 24 hours and may not accurately reflect your invoice. +

    +
    diff --git a/src/routes/console/organization-[organization]/usage/[[invoice]]/+page.ts b/src/routes/console/organization-[organization]/usage/[[invoice]]/+page.ts new file mode 100644 index 000000000..b1ef9a272 --- /dev/null +++ b/src/routes/console/organization-[organization]/usage/[[invoice]]/+page.ts @@ -0,0 +1,63 @@ +import { sdk } from '$lib/stores/sdk'; +import { Query } from '@appwrite.io/console'; +import type { PageLoad } from './$types'; +import type { Organization } from '$lib/stores/organization'; +import type { Invoice } from '$lib/sdk/billing'; + +export const load: PageLoad = async ({ params, parent }) => { + const { invoice } = params; + const parentData = await parent(); + const org = parentData.organization as Organization; + + /** + * Temporary fix during migration to billing system + */ + if (org.billingCurrentInvoiceDate === null || org.billingNextInvoiceDate === null) { + return { + organizationUsage: { + bandwidth: null, + users: null, + usersTotal: null, + storageTotal: null, + executions: null, + executionsTotal: null, + projects: null + } + }; + } + + let startDate: string = org.billingCurrentInvoiceDate; + let endDate: string = org.billingNextInvoiceDate; + let currentInvoice: Invoice = undefined; + + if (invoice) { + currentInvoice = await sdk.forConsole.billing.getInvoice(org.$id, invoice); + startDate = currentInvoice.from; + endDate = currentInvoice.to; + } + + const [invoices, usage] = await Promise.all([ + sdk.forConsole.billing.listInvoices(org.$id, [Query.orderDesc('from')]), + sdk.forConsole.billing.listUsage(params.organization, startDate, endDate) + ]); + + const queries: string[] = []; + + if (usage?.projects?.length > 0) { + queries.push( + Query.equal( + '$id', + usage.projects.map((p) => p.projectId) + ) + ); + } + + const projectNames = await sdk.forConsole.projects.list(queries); + + return { + organizationUsage: usage, + projectNames: projectNames.projects, + invoices, + currentInvoice + }; +}; diff --git a/src/routes/console/organization-[organization]/usage/[[invoice]]/ProjectBreakdown.svelte b/src/routes/console/organization-[organization]/usage/[[invoice]]/ProjectBreakdown.svelte new file mode 100644 index 000000000..456559272 --- /dev/null +++ b/src/routes/console/organization-[organization]/usage/[[invoice]]/ProjectBreakdown.svelte @@ -0,0 +1,81 @@ + + + + + Project breakdown + + + Project + Usage + + + + {#each groupByProject(metric).sort((a, b) => b.usage - a.usage) as project} + + + {getProjectName(project.projectId)} + + {format(project.usage)} + + View project usage + + + {/each} + + + + diff --git a/src/routes/console/organization-[organization]/wizard/step1.svelte b/src/routes/console/organization-[organization]/wizard/step1.svelte new file mode 100644 index 000000000..70bbc0050 --- /dev/null +++ b/src/routes/console/organization-[organization]/wizard/step1.svelte @@ -0,0 +1,37 @@ + + + + Project Details + + + + {#if !showCustomId} +
    + (showCustomId = !showCustomId)}> + +
    + {:else} + + {/if} +
    +
    diff --git a/src/routes/console/organization-[organization]/wizard/step2.svelte b/src/routes/console/organization-[organization]/wizard/step2.svelte new file mode 100644 index 000000000..290f90acd --- /dev/null +++ b/src/routes/console/organization-[organization]/wizard/step2.svelte @@ -0,0 +1,127 @@ + + + + Regions + + Choose a deployment region for your project. This region cannot be changed. + + {#if regions?.total} +
      + {#each regions.regions.filter((r) => r.$id !== 'default') as region} +
    • + +
      + {#if region.disabled} + +

      {region.name}

      + {#if !notifications.includes(region.$id)} + { + notifyRegion(region); + }}> + + {:else} + { + unNotifyRegion(region); + }}> + + {/if} + {:else} + + + {region.name} + {/if} +
      +
      +
    • + {/each} +
    + {/if} +
    diff --git a/src/routes/console/organization-[organization]/wizard/store.ts b/src/routes/console/organization-[organization]/wizard/store.ts new file mode 100644 index 000000000..d79bd1ded --- /dev/null +++ b/src/routes/console/organization-[organization]/wizard/store.ts @@ -0,0 +1,11 @@ +import { writable } from 'svelte/store'; + +export const createProject = writable<{ + id?: string; + name: string; + region: string; +}>({ + id: null, + name: null, + region: 'fra' +}); diff --git a/src/routes/console/project-[project]/+layout.ts b/src/routes/console/project-[project]/+layout.ts index cd22bca7b..98096bd90 100644 --- a/src/routes/console/project-[project]/+layout.ts +++ b/src/routes/console/project-[project]/+layout.ts @@ -3,15 +3,23 @@ import { sdk } from '$lib/stores/sdk'; import { error } from '@sveltejs/kit'; import type { LayoutLoad } from './$types'; import { preferences } from '$lib/stores/preferences'; +import { failedInvoice } from '$lib/stores/billing'; +import { isCloud } from '$lib/system'; export const load: LayoutLoad = async ({ params, depends }) => { depends(Dependencies.PROJECT); try { const project = await sdk.forConsole.projects.get(params.project); - sdk.forConsole.account.updatePrefs({ organization: project.teamId }); + const prefs = await sdk.forConsole.account.getPrefs(); + const newPrefs = { ...prefs, organization: project.teamId }; + sdk.forConsole.account.updatePrefs(newPrefs); preferences.loadTeamPrefs(project.teamId); + if (isCloud) { + await failedInvoice.load(project.teamId); + } + return { project, organization: await sdk.forConsole.teams.get(project.teamId) diff --git a/src/routes/console/project-[project]/auth/+layout.svelte b/src/routes/console/project-[project]/auth/+layout.svelte index 69c53375a..cfb6a198b 100644 --- a/src/routes/console/project-[project]/auth/+layout.svelte +++ b/src/routes/console/project-[project]/auth/+layout.svelte @@ -2,6 +2,8 @@ import { goto } from '$app/navigation'; import { page } from '$app/stores'; import { registerCommands, updateCommandGroupRanks } from '$lib/commandCenter'; + import { readOnly } from '$lib/stores/billing'; + import { GRACE_PERIOD_OVERRIDE } from '$lib/system'; import { project } from '../store'; import { showCreateUser } from './+page.svelte'; import { showCreateTeam } from './teams/+page.svelte'; @@ -16,10 +18,10 @@ showCreateUser.set(true); }, keys: $page.url.pathname.endsWith('auth') ? ['c'] : ['c', 'u'], - group: 'users', icon: 'plus', - rank: $page.url.pathname.endsWith('auth') ? 10 : 0 + rank: $page.url.pathname.endsWith('auth') ? 10 : 0, + disabled: $readOnly && !GRACE_PERIOD_OVERRIDE }, { label: 'Create team', @@ -33,7 +35,8 @@ group: 'teams', icon: 'plus', - rank: $page.url.pathname.endsWith('teams') ? 10 : 0 + rank: $page.url.pathname.endsWith('teams') ? 10 : 0, + disabled: $readOnly && !GRACE_PERIOD_OVERRIDE }, { label: 'Go to teams', diff --git a/src/routes/console/project-[project]/auth/+page.svelte b/src/routes/console/project-[project]/auth/+page.svelte index 0d02e3a2d..209c3d37d 100644 --- a/src/routes/console/project-[project]/auth/+page.svelte +++ b/src/routes/console/project-[project]/auth/+page.svelte @@ -26,11 +26,12 @@ TableRowLink } from '$lib/elements/table'; import { toLocaleDate, toLocaleDateTime } from '$lib/helpers/date'; - import { Container } from '$lib/layout'; + import { Container, ContainerHeader } from '$lib/layout'; import type { Models } from '@appwrite.io/console'; import { writable } from 'svelte/store'; import type { PageData } from './$types'; import Create from './createUser.svelte'; + import { tooltip } from '$lib/actions/tooltip'; export let data: PageData; @@ -41,11 +42,22 @@ - - - + + +
    + +
    +
    {#if data.users.total} diff --git a/src/routes/console/project-[project]/auth/store.ts b/src/routes/console/project-[project]/auth/store.ts new file mode 100644 index 000000000..ae7e544db --- /dev/null +++ b/src/routes/console/project-[project]/auth/store.ts @@ -0,0 +1,8 @@ +import { page } from '$app/stores'; +import type { Models } from '@appwrite.io/console'; +import { derived } from 'svelte/store'; + +export const userList = derived( + page, + ($page) => $page.data.users as unknown as Models.UserList> +); diff --git a/src/routes/console/project-[project]/auth/teams/+page.svelte b/src/routes/console/project-[project]/auth/teams/+page.svelte index ba925b078..5aa93c321 100644 --- a/src/routes/console/project-[project]/auth/teams/+page.svelte +++ b/src/routes/console/project-[project]/auth/teams/+page.svelte @@ -24,11 +24,14 @@ import Create from '../createTeam.svelte'; import { goto } from '$app/navigation'; import { toLocaleDateTime } from '$lib/helpers/date'; - import { Container } from '$lib/layout'; + import { Container, ContainerHeader } from '$lib/layout'; import { base } from '$app/paths'; import type { Models } from '@appwrite.io/console'; import type { PageData } from './$types'; + import { tooltip } from '$lib/actions/tooltip'; import { writable } from 'svelte/store'; + import { readOnly } from '$lib/stores/billing'; + import { isCloud } from '$lib/system'; export let data: PageData; @@ -39,11 +42,29 @@ - - - + + +
    + +
    +
    +
    + {#if data.teams.total}
    @@ -51,7 +72,7 @@ Members Created - + {#each data.teams.teams as team} diff --git a/src/routes/console/project-[project]/auth/teams/team-[team]/createMembership.svelte b/src/routes/console/project-[project]/auth/teams/team-[team]/createMembership.svelte index 295910ba3..4d4fab614 100644 --- a/src/routes/console/project-[project]/auth/teams/team-[team]/createMembership.svelte +++ b/src/routes/console/project-[project]/auth/teams/team-[team]/createMembership.svelte @@ -22,10 +22,10 @@ const user = await sdk.forProject.teams.createMembership( teamId, roles, - url, email || undefined, undefined, undefined, + url, name || undefined ); addNotification({ diff --git a/src/routes/console/project-[project]/auth/templates/+page.svelte b/src/routes/console/project-[project]/auth/templates/+page.svelte index d72167797..6d686d883 100644 --- a/src/routes/console/project-[project]/auth/templates/+page.svelte +++ b/src/routes/console/project-[project]/auth/templates/+page.svelte @@ -40,6 +40,10 @@ // import { baseEmailTemplate, baseSmsTemplate, emailTemplate, smsTemplate } from './store'; import { baseEmailTemplate, emailTemplate } from './store'; import { Button } from '$lib/elements/forms'; + import { organization } from '$lib/stores/organization'; + import ChangeOrganizationTierCloud from '$routes/console/changeOrganizationTierCloud.svelte'; + import { wizard } from '$lib/stores/wizard'; + import { BillingPlan } from '$lib/constants'; const projectId = $page.params.project; @@ -114,6 +118,18 @@

    + {#if $organization.billingPlan === BillingPlan.STARTER} + wizard.start(ChangeOrganizationTierCloud) + } + ]}> + All emails sent using the Starter plan will include attribution to Appwrite in + the signature. To send attribution-free emails, upgrade your plan. + + {/if} - +
    Template language diff --git a/src/routes/console/project-[project]/auth/usage/[[period]]/+page.svelte b/src/routes/console/project-[project]/auth/usage/[[period]]/+page.svelte index 9c44940be..c8ffc9c54 100644 --- a/src/routes/console/project-[project]/auth/usage/[[period]]/+page.svelte +++ b/src/routes/console/project-[project]/auth/usage/[[period]]/+page.svelte @@ -11,9 +11,9 @@ + }} + {total} + {count} /> diff --git a/src/routes/console/project-[project]/auth/user-[user]/activity/+page.svelte b/src/routes/console/project-[project]/auth/user-[user]/activity/+page.svelte index 4870c8b9b..985496c75 100644 --- a/src/routes/console/project-[project]/auth/user-[user]/activity/+page.svelte +++ b/src/routes/console/project-[project]/auth/user-[user]/activity/+page.svelte @@ -5,4 +5,4 @@ export let data: PageData; - + diff --git a/src/routes/console/project-[project]/auth/user-[user]/deleteUser.svelte b/src/routes/console/project-[project]/auth/user-[user]/deleteUser.svelte index 9b3148270..fd769a16a 100644 --- a/src/routes/console/project-[project]/auth/user-[user]/deleteUser.svelte +++ b/src/routes/console/project-[project]/auth/user-[user]/deleteUser.svelte @@ -11,7 +11,7 @@ import { Submit, trackEvent, trackError } from '$lib/actions/analytics'; export let showDelete = false; - + let error: string; const deleteUser = async () => { try { await sdk.forProject.users.delete($user.$id); @@ -22,12 +22,9 @@ }); trackEvent(Submit.UserDelete); await goto(`${base}/console/project-${$page.params.project}/auth`); - } catch (error) { - addNotification({ - type: 'error', - message: error.message - }); - trackError(error, Submit.UserDelete); + } catch (e) { + error = e.message; + trackError(e, Submit.UserDelete); } }; @@ -38,7 +35,8 @@ onSubmit={deleteUser} icon="exclamation" state="warning" - headerDivider={false}> + headerDivider={false} + bind:error>

    Are you sure you want to delete {$user.name} from '{$project.name}'?

    diff --git a/src/routes/console/project-[project]/auth/user-[user]/sessions/+page.svelte b/src/routes/console/project-[project]/auth/user-[user]/sessions/+page.svelte index 793bd36a8..e58c335cd 100644 --- a/src/routes/console/project-[project]/auth/user-[user]/sessions/+page.svelte +++ b/src/routes/console/project-[project]/auth/user-[user]/sessions/+page.svelte @@ -3,7 +3,7 @@ import { Pill } from '$lib/elements'; import { Button } from '$lib/elements/forms'; import { - Table, + TableScroll, TableBody, TableCell, TableCellHead, @@ -11,7 +11,7 @@ TableHeader, TableRow } from '$lib/elements/table'; - import { Container } from '$lib/layout'; + import { Container, ContainerHeader } from '$lib/layout'; import { sdk } from '$lib/stores/sdk'; import DeleteAllSessions from '../deleteAllSessions.svelte'; import DeleteSessions from '../deleteSession.svelte'; @@ -28,13 +28,15 @@ - {#if data.sessions.total} -
    + + {#if data.sessions.total} -
    -
    + {/if} + + {#if data.sessions.total} + Browser and device Session @@ -83,7 +85,7 @@ {/each} -
    + {:else}
    diff --git a/src/routes/console/project-[project]/databases/+page.svelte b/src/routes/console/project-[project]/databases/+page.svelte index b3c623013..e70630db2 100644 --- a/src/routes/console/project-[project]/databases/+page.svelte +++ b/src/routes/console/project-[project]/databases/+page.svelte @@ -2,9 +2,9 @@ import { goto } from '$app/navigation'; import { base } from '$app/paths'; import { page } from '$app/stores'; - import { Empty, PaginationWithLimit } from '$lib/components'; + import { Empty, PaginationWithLimit, ViewSelector } from '$lib/components'; import { Button } from '$lib/elements/forms'; - import { Container, GridHeader } from '$lib/layout'; + import { Container, ContainerHeader } from '$lib/layout'; import type { Models } from '@appwrite.io/console'; import type { PageData } from './$types'; @@ -13,10 +13,12 @@ import { columns } from './store'; import Table from './table.svelte'; import { registerCommands } from '$lib/commandCenter'; + import { tooltip } from '$lib/actions/tooltip'; export let data: PageData; let showCreate = false; + let isCreationDisabled = false; const project = $page.params.project; async function handleCreate(event: CustomEvent) { @@ -31,7 +33,7 @@ showCreate = true; }, keys: ['c'], - disabled: showCreate, + disabled: showCreate || isCreationDisabled, icon: 'plus', group: 'databases', rank: 10 @@ -40,17 +42,35 @@ - - - + total={data?.databases?.total} + on:data={(data) => { + isCreationDisabled = data.detail.isButtonDisabled; + }}> + +
    + +
    + +
    +
    +
    + {#if data.databases.total} {#if data.view === 'grid'} diff --git a/src/routes/console/project-[project]/databases/database-[database]/collection-[collection]/document-[document]/+page.svelte b/src/routes/console/project-[project]/databases/database-[database]/collection-[collection]/document-[document]/+page.svelte index 738d87c96..0ee809bd9 100644 --- a/src/routes/console/project-[project]/databases/database-[database]/collection-[collection]/document-[document]/+page.svelte +++ b/src/routes/console/project-[project]/databases/database-[database]/collection-[collection]/document-[document]/+page.svelte @@ -68,9 +68,15 @@ Permissions

    - Assign read or write permissions at the collection level or - document level. If collection level permissions are assigned, permissions applied - to individual documents are ignored. + A user requires appropriate permissions at either the collection level or + document level to access a document. If no permissions are configured, no user + can access the document + Learn more about database permissions.

    diff --git a/src/routes/console/project-[project]/databases/grid.svelte b/src/routes/console/project-[project]/databases/grid.svelte index d4066b928..030241ac4 100644 --- a/src/routes/console/project-[project]/databases/grid.svelte +++ b/src/routes/console/project-[project]/databases/grid.svelte @@ -8,7 +8,11 @@ const project = $page.params.project; - (showCreate = true)} event="database"> + (showCreate = true)} + event="database" + service="databases"> {#each data.databases.databases as database} {database.name} diff --git a/src/routes/console/project-[project]/functions/+page.svelte b/src/routes/console/project-[project]/functions/+page.svelte index 55c67f9e9..989f8ebab 100644 --- a/src/routes/console/project-[project]/functions/+page.svelte +++ b/src/routes/console/project-[project]/functions/+page.svelte @@ -1,18 +1,10 @@ -
    - Functions - -
    + {#if data.functions.total} + on:click={openWizard} + service="functions"> {#each data.functions.functions as func} @@ -130,7 +127,7 @@ {:else} {/if} diff --git a/src/routes/console/project-[project]/functions/function-[function]/+page.svelte b/src/routes/console/project-[project]/functions/function-[function]/+page.svelte index 008481569..1462e407d 100644 --- a/src/routes/console/project-[project]/functions/function-[function]/+page.svelte +++ b/src/routes/console/project-[project]/functions/function-[function]/+page.svelte @@ -26,7 +26,7 @@ TableScroll } from '$lib/elements/table'; import { deploymentList, execute, func, proxyRuleList } from './store'; - import { Container } from '$lib/layout'; + import { Container, ContainerHeader } from '$lib/layout'; import { app } from '$lib/stores/app'; import { calculateSize, humanFileSize } from '$lib/helpers/sizeConvertion'; import type { Models } from '@appwrite.io/console'; @@ -39,6 +39,8 @@ import DeploymentSource from './deploymentSource.svelte'; import DeploymentCreatedBy from './deploymentCreatedBy.svelte'; import DeploymentDomains from './deploymentDomains.svelte'; + import { GRACE_PERIOD_OVERRIDE, isCloud } from '$lib/system'; + import { readOnly } from '$lib/stores/billing'; export let data; @@ -56,10 +58,9 @@ -
    - Deployments + -
    +
    {#if $deploymentList?.total} {@const activeDeployment = data.activeDeployment}
    @@ -148,7 +149,12 @@ Redeploy - +
    diff --git a/src/routes/console/project-[project]/functions/function-[function]/executions/+page.svelte b/src/routes/console/project-[project]/functions/function-[function]/executions/+page.svelte index 461982d30..6db3add0d 100644 --- a/src/routes/console/project-[project]/functions/function-[function]/executions/+page.svelte +++ b/src/routes/console/project-[project]/functions/function-[function]/executions/+page.svelte @@ -1,7 +1,7 @@ -
    - Executions + (selectedFunction = $func)}> + +

    The {tier} plan has limits

    +
      +
    • + {abbreviateNumber(limit)} function executions +
    • +
    • + {logs} hour of logs +
    • +
    + {#if $organization?.billingPlan === BillingPlan.STARTER} +

    + + to increase your resource limits. +

    + {:else} +

    + After this amount, . +

    + {/if} +
    +
    - -
    {#if !$func.logging}
    @@ -139,4 +170,6 @@ {/if} + + diff --git a/src/routes/console/project-[project]/functions/function-[function]/settings/updateSchedule.svelte b/src/routes/console/project-[project]/functions/function-[function]/settings/updateSchedule.svelte index a347cf509..514a76722 100644 --- a/src/routes/console/project-[project]/functions/function-[function]/settings/updateSchedule.svelte +++ b/src/routes/console/project-[project]/functions/function-[function]/settings/updateSchedule.svelte @@ -58,7 +58,7 @@ Schedule

    Set a Cron schedule to trigger your function. Leave blank for no schedule. diff --git a/src/routes/console/project-[project]/functions/function-[function]/usage/[[period]]/+page.svelte b/src/routes/console/project-[project]/functions/function-[function]/usage/[[period]]/+page.svelte index c71a6d1ad..c486d2ba5 100644 --- a/src/routes/console/project-[project]/functions/function-[function]/usage/[[period]]/+page.svelte +++ b/src/routes/console/project-[project]/functions/function-[function]/usage/[[period]]/+page.svelte @@ -4,6 +4,7 @@ import { BarChart } from '$lib/charts'; import { page } from '$app/stores'; import type { PageData } from './$types'; + import { formatNumberWithCommas } from '$lib/helpers/numbers'; export let data: PageData; $: total = data.executionsTotal; @@ -33,7 +34,7 @@

    {#if count} - {total} + {formatNumberWithCommas(total)}

    Executions

    { } return { runtimes: [...baseRuntimes.values()] }; }); + +export const functionsList = derived(page, ($page) => $page.data.functions as Models.FunctionList); diff --git a/src/routes/console/project-[project]/functions/templates/+page.svelte b/src/routes/console/project-[project]/functions/templates/+page.svelte index 110cb04fb..c1a0ba1f6 100644 --- a/src/routes/console/project-[project]/functions/templates/+page.svelte +++ b/src/routes/console/project-[project]/functions/templates/+page.svelte @@ -12,10 +12,13 @@ SvgIcon } from '$lib/components'; import { Button, InputSearch } from '$lib/elements/forms'; - import { Container } from '$lib/layout'; + import { Container, ContainerButton } from '$lib/layout'; import { app } from '$lib/stores/app'; + import { isServiceLimited } from '$lib/stores/billing'; import type { Runtime } from '$lib/stores/marketplace.js'; + import { organization } from '$lib/stores/organization'; import { connectTemplate } from '$lib/wizards/functions/cover.svelte'; + import { functionsList } from '../store'; export let data; @@ -83,6 +86,12 @@ target.searchParams.delete('page'); goto(target.toString(), { keepFocus: true }); } + + $: buttonDisabled = isServiceLimited( + 'functions', + $organization?.billingPlan, + $functionsList?.total ?? 0 + ); @@ -218,9 +227,14 @@ View details - + connectTemplate(template)} + showIcon={false} + buttonText="Create function" + buttonEvent="create_function" />
    diff --git a/src/routes/console/project-[project]/functions/templates/+page.ts b/src/routes/console/project-[project]/functions/templates/+page.ts index b4ae67288..3ce6a8184 100644 --- a/src/routes/console/project-[project]/functions/templates/+page.ts +++ b/src/routes/console/project-[project]/functions/templates/+page.ts @@ -1,9 +1,12 @@ -import { CARD_LIMIT } from '$lib/constants'; +import { CARD_LIMIT, Dependencies } from '$lib/constants'; import { getPage, getSearch, getView, pageToOffset, View } from '$lib/helpers/load'; import { marketplace } from '$lib/stores/marketplace'; +import { sdk } from '$lib/stores/sdk'; import type { PageLoad } from './$types'; -export const load: PageLoad = async ({ url, route }) => { +export const load: PageLoad = async ({ url, route, depends }) => { + depends(Dependencies.FUNCTIONS); + const limit = CARD_LIMIT; const page = getPage(url); const search = getSearch(url); @@ -52,6 +55,7 @@ export const load: PageLoad = async ({ url, route }) => { runtimes, useCases, sum: templates.length, - templates: templates.splice(((page === 0 ? 1 : page) - 1) * limit, limit) + templates: templates.splice(((page === 0 ? 1 : page) - 1) * limit, limit), + functions: await sdk.forProject.functions.list() }; }; diff --git a/src/routes/console/project-[project]/functions/templates/template-[template]/+page.svelte b/src/routes/console/project-[project]/functions/templates/template-[template]/+page.svelte index 15e43c994..fc6e5ec74 100644 --- a/src/routes/console/project-[project]/functions/templates/template-[template]/+page.svelte +++ b/src/routes/console/project-[project]/functions/templates/template-[template]/+page.svelte @@ -2,7 +2,7 @@ import { Alert, Card, Collapsible, Heading } from '$lib/components'; import { Pill } from '$lib/elements'; import { Button } from '$lib/elements/forms'; - import { Container } from '$lib/layout'; + import { Container, ContainerButton } from '$lib/layout'; import { isSelfHosted } from '$lib/system'; import AppwriteLogoDark from '$lib/images/appwrite-logo-dark.svg'; import AppwriteLogoLight from '$lib/images/appwrite-logo-light.svg'; @@ -10,8 +10,17 @@ import { consoleVariables } from '$routes/console/store'; import { template } from './store'; import { app } from '$lib/stores/app'; + import { isServiceLimited } from '$lib/stores/billing'; + import { organization } from '$lib/stores/organization'; + import { functionsList } from '../../store'; const isVcsEnabled = $consoleVariables?._APP_VCS_ENABLED === true; + + $: buttonDisabled = isServiceLimited( + 'functions', + $organization?.billingPlan, + $functionsList?.total + ); @@ -90,11 +99,13 @@ View source - + connectTemplate($template)} + showIcon={false} + buttonText="Create function" + buttonEvent="create_function" />
    diff --git a/src/routes/console/project-[project]/functions/templates/template-[template]/+page.ts b/src/routes/console/project-[project]/functions/templates/template-[template]/+page.ts index 7936cc751..0c7a6dec4 100644 --- a/src/routes/console/project-[project]/functions/templates/template-[template]/+page.ts +++ b/src/routes/console/project-[project]/functions/templates/template-[template]/+page.ts @@ -1,8 +1,13 @@ +import { Dependencies } from '$lib/constants'; import { marketplace } from '$lib/stores/marketplace'; +import { sdk } from '$lib/stores/sdk'; import type { PageLoad } from './$types'; -export const load: PageLoad = async ({ params }) => { +export const load: PageLoad = async ({ params, depends }) => { + depends(Dependencies.FUNCTIONS); + return { - template: marketplace.find((template) => template.id === params.template) + template: marketplace.find((template) => template.id === params.template), + functions: await sdk.forProject.functions.list() }; }; diff --git a/src/routes/console/project-[project]/functions/wizard/step1.svelte b/src/routes/console/project-[project]/functions/wizard/step1.svelte index 1878c57d6..17408aacb 100644 --- a/src/routes/console/project-[project]/functions/wizard/step1.svelte +++ b/src/routes/console/project-[project]/functions/wizard/step1.svelte @@ -27,6 +27,7 @@ label="Name" id="name" placeholder="Function name" + autofocus bind:value={$createFunction.name} required /> Schedule Set a Cron schedule to trigger your function. Leave blank for no schedule. More details on Cron syntax here @import '@appwrite.io/pink/src/abstract/variables/_devices.scss'; + .card { + background: #fbfbfc; + + :global(.theme-dark) & { + background: #19191c !important; + } + } @media #{$break2open} { .card-header { background-image: var(--url); diff --git a/src/routes/console/project-[project]/overview/platforms/+page.svelte b/src/routes/console/project-[project]/overview/platforms/+page.svelte index e9829bdd0..b7f8dfab0 100644 --- a/src/routes/console/project-[project]/overview/platforms/+page.svelte +++ b/src/routes/console/project-[project]/overview/platforms/+page.svelte @@ -36,6 +36,7 @@ import { toLocaleDateTime } from '$lib/helpers/date'; import { app } from '$lib/stores/app'; import type { PageData } from './$types'; + import { ContainerHeader } from '$lib/layout'; export let data: PageData; @@ -72,27 +73,25 @@ }; -
    - Platforms - - - - - addPlatform(Platform.Web)}>Web App - addPlatform(Platform.Flutter)}> - Flutter App - - addPlatform(Platform.Android)}> - Android App - - addPlatform(Platform.Apple)}>Apple App - - - -
    + + + + + addPlatform(Platform.Web)}>Web App + addPlatform(Platform.Flutter)}>Flutter App + addPlatform(Platform.Android)}>Android App + addPlatform(Platform.Apple)}>Apple App + + + {#if data.platforms.platforms.length}
    addPlatform(Platform.Web)}> Web - Ï + addPlatform(Platform.Flutter)}> Flutter diff --git a/src/routes/console/project-[project]/overview/platforms/wizard/android/step3.svelte b/src/routes/console/project-[project]/overview/platforms/wizard/android/step3.svelte index 01c74f02f..736d87555 100644 --- a/src/routes/console/project-[project]/overview/platforms/wizard/android/step3.svelte +++ b/src/routes/console/project-[project]/overview/platforms/wizard/android/step3.svelte @@ -23,11 +23,14 @@ val client = Client(context) value={project}>Project ID.

    - +
    + +
    diff --git a/src/routes/console/project-[project]/overview/platforms/wizard/flutter/step2.svelte b/src/routes/console/project-[project]/overview/platforms/wizard/flutter/step2.svelte index 2f1c10431..6bd6f9e68 100644 --- a/src/routes/console/project-[project]/overview/platforms/wizard/flutter/step2.svelte +++ b/src/routes/console/project-[project]/overview/platforms/wizard/flutter/step2.svelte @@ -19,5 +19,7 @@

    You can also install the SDK using the Dart package manager from your terminal.

    - +
    + +
    diff --git a/src/routes/console/project-[project]/overview/platforms/wizard/flutter/step3.svelte b/src/routes/console/project-[project]/overview/platforms/wizard/flutter/step3.svelte index 95e585413..cc82830e2 100644 --- a/src/routes/console/project-[project]/overview/platforms/wizard/flutter/step3.svelte +++ b/src/routes/console/project-[project]/overview/platforms/wizard/flutter/step3.svelte @@ -25,7 +25,16 @@ client Initialize your SDK by pointing the client to your Appwrite project using your Project ID

    - +
    + +

    Before sending any API calls to your new Appwrite project, make sure your device or emulator has network access to your Appwrite project's hostname or IP address. diff --git a/src/routes/console/project-[project]/overview/platforms/wizard/web/step3.svelte b/src/routes/console/project-[project]/overview/platforms/wizard/web/step3.svelte index de3a41e49..c259253dd 100644 --- a/src/routes/console/project-[project]/overview/platforms/wizard/web/step3.svelte +++ b/src/routes/console/project-[project]/overview/platforms/wizard/web/step3.svelte @@ -21,5 +21,14 @@ client value={project}>Project ID.

    - +
    + +
    diff --git a/src/routes/console/project-[project]/overview/store.ts b/src/routes/console/project-[project]/overview/store.ts index 9a1734507..ea055c522 100644 --- a/src/routes/console/project-[project]/overview/store.ts +++ b/src/routes/console/project-[project]/overview/store.ts @@ -1,19 +1,17 @@ import { sdk } from '$lib/stores/sdk'; import { cachedStore } from '$lib/helpers/cache'; -import type { UsageProject } from '$lib/sdk/usage'; import { writable, type Writable } from 'svelte/store'; +import type { Models } from '@appwrite.io/console'; export const usage = cachedStore< - UsageProject, + Models.UsageProject, { - load: (range: string) => Promise; + load: (start: string, end: string, period: '1h' | '1d') => Promise; } >('projectUsage', function ({ set }) { return { - load: async (range) => { - const usages: UsageProject = (await sdk.forProject.project.getUsage( - range - )) as unknown as UsageProject; + load: async (start, end, period) => { + const usages = await sdk.forProject.project.getUsage(start, end, period); set(usages); } }; diff --git a/src/routes/console/project-[project]/settings/GitDisconnectModal.svelte b/src/routes/console/project-[project]/settings/GitDisconnectModal.svelte index 0e4f978a1..b414f37bd 100644 --- a/src/routes/console/project-[project]/settings/GitDisconnectModal.svelte +++ b/src/routes/console/project-[project]/settings/GitDisconnectModal.svelte @@ -18,7 +18,7 @@ await sdk.forProject.vcs.deleteInstallation(selectedInstallation.$id); await invalidate(Dependencies.PROJECT_INSTALLATIONS); addNotification({ - message: `${selectedInstallation.organization} has been disconnected from your project`, + message: `${selectedInstallation.organization} has been disconnected from this project`, type: 'success' }); trackEvent(Submit.InstallationDelete, { diff --git a/src/routes/console/project-[project]/settings/deleteProject.svelte b/src/routes/console/project-[project]/settings/deleteProject.svelte index 065e9eed8..b4d40ad24 100644 --- a/src/routes/console/project-[project]/settings/deleteProject.svelte +++ b/src/routes/console/project-[project]/settings/deleteProject.svelte @@ -7,11 +7,21 @@ import { toLocaleDateTime } from '$lib/helpers/date'; import { addNotification } from '$lib/stores/notifications'; import { sdk } from '$lib/stores/sdk'; + import { isCloud } from '$lib/system'; + import { onMount } from 'svelte'; import { project } from '../store'; + import type { RegionList } from '$lib/sdk/billing'; let showDelete = false; let name: string = null; + let regions: RegionList; + onMount(async () => { + if (isCloud) { + regions = await sdk.forConsole.billing.listRegions(); + } + }); + const handleDelete = async () => { try { await sdk.forConsole.projects.delete($project.$id); @@ -45,6 +55,10 @@
    {$project.name}
    + {#if isCloud && $project.region && regions?.total} + {@const projectRegion = regions.regions.find((r) => r.$id === $project?.region)} +

    Region: {projectRegion.name}

    + {/if}

    Last update: {toLocaleDateTime($project.$updatedAt)}

    diff --git a/src/routes/console/project-[project]/settings/disableAllServices.svelte b/src/routes/console/project-[project]/settings/disableAllServices.svelte index d051fff4d..4c81f671e 100644 --- a/src/routes/console/project-[project]/settings/disableAllServices.svelte +++ b/src/routes/console/project-[project]/settings/disableAllServices.svelte @@ -13,8 +13,8 @@ bind:show onSubmit={handleDisableAll}>

    - Are you sure you want to disable all services? This will disable API requests to your - project. + Are you sure you want to disable all services? This will disable API requests to this + project for all Client SDKs.

    diff --git a/src/routes/console/project-[project]/settings/header.svelte b/src/routes/console/project-[project]/settings/header.svelte index 5015df978..0a733acd1 100644 --- a/src/routes/console/project-[project]/settings/header.svelte +++ b/src/routes/console/project-[project]/settings/header.svelte @@ -31,6 +31,12 @@ href: `${path}/smtp`, title: 'SMTP', event: 'smtp' + }, + { + href: `${path}/usage`, + title: 'Usage', + event: 'usage', + hasChildren: true } ]; diff --git a/src/routes/console/project-[project]/settings/migrations/+page.svelte b/src/routes/console/project-[project]/settings/migrations/+page.svelte index 7a9863793..2a7877a02 100644 --- a/src/routes/console/project-[project]/settings/migrations/+page.svelte +++ b/src/routes/console/project-[project]/settings/migrations/+page.svelte @@ -15,7 +15,7 @@ import { isSameDay, toLocaleDate } from '$lib/helpers/date'; import { Container } from '$lib/layout'; import { sdk } from '$lib/stores/sdk'; - import { isSelfHosted } from '$lib/system'; + import { GRACE_PERIOD_OVERRIDE, isSelfHosted } from '$lib/system'; import { onMount } from 'svelte'; import { project } from '../../store'; import { openImportWizard } from './(import)'; @@ -23,6 +23,7 @@ import ExportModal from './exportModal.svelte'; import Status from '$lib/components/status.svelte'; import { capitalize } from '$lib/helpers/string'; + import { readOnly } from '$lib/stores/billing'; export let data; let details: string | null = null; @@ -222,7 +223,11 @@
    -
    @@ -251,8 +256,9 @@
    - + diff --git a/src/routes/console/project-[project]/settings/smtp/+page.svelte b/src/routes/console/project-[project]/settings/smtp/+page.svelte index 832f48baa..e61255b17 100644 --- a/src/routes/console/project-[project]/settings/smtp/+page.svelte +++ b/src/routes/console/project-[project]/settings/smtp/+page.svelte @@ -1,5 +1,5 @@ + + +
    + Usage + + {#if $organization?.billingPlan === BillingPlan.STARTER} + + {/if} +
    +
    + {#if $organization.billingPlan === BillingPlan.SCALE} +

    + On the Scale plan, you'll be charged only for any usage that exceeds the thresholds + per resource listed below. +

    + {:else if $organization.billingPlan === BillingPlan.PRO} +

    + On the Pro plan, you'll be charged only for any usage that exceeds the thresholds + per resource listed below. +

    + {:else if $organization.billingPlan === BillingPlan.STARTER} +

    + If you exceed the limits of the {plan} plan, services for your projects may be disrupted. + . +

    + {/if} + + +
    + + Bandwidth +

    Calculated for all bandwidth used in your project.

    + + {#if network} + {@const humanized = humanFileSize(total(network))} +
    + {humanized.value} + {humanized.unit} +
    + + value + ? `${humanFileSize(+value).value} ${ + humanFileSize(+value).unit + }` + : '0' + } + } + }} + series={[ + { + name: 'Bandwidth', + data: [...network.map((e) => [e.date, e.value])], + tooltip: { + valueFormatter: (value) => + `${humanFileSize(+value).value} ${humanFileSize(+value).unit}` + } + } + ]} /> + {:else} + +
    +
    +
    + {/if} +
    +
    + + Users + +

    Total user in your project.

    + + + {#if users} + {@const current = formatNum(usersTotal)} +
    +
    +

    + {current} + Users +

    +
    +
    + [e.date, e.value])] + } + ]} /> + {:else} + +
    +
    +
    + {/if} +
    +
    + + Executions + +

    + Calculated for all functions that are executed in all projects in your project. +

    + + + {#if executions} + {@const current = formatNum(executionsTotal)} +
    +
    +

    + {current} + Executions +

    +
    +
    + [e.date, e.value])] + } + ]} /> + {#if data.usage.executionsBreakdown.length > 0} + + + Function + Usage + + + + {#each data.usage.executionsBreakdown as func} + + + {func.name ?? func.resourceId} + + + {formatNum(func.value)} executions + + + View function + + + {/each} + +
    + {/if} + {:else} + +
    +
    +
    + {/if} +
    +
    + + Storage + +

    + Calculated for all your files, deployments, builds and databases. While in beta, only + file storage is counted against your plan limits. +

    + + + {#if storage} + {@const humanized = humanFileSize(storage)} +
    +
    +

    + {humanized.value} + {humanized.unit} +

    +
    +
    + {#if data.usage.bucketsBreakdown.length > 0} + + + Bucket + Usage + + + + {#each data.usage.bucketsBreakdown.sort((a, b) => b.value - a.value) as bucket} + {@const humanized = humanFileSize(bucket.value)} + + + {bucket.name ?? bucket.resourceId} + + + {humanized.value}{humanized.unit} + + + View bucket + + + {/each} + +
    + {/if} + {:else} + +
    +
    +
    + {/if} +
    +
    +

    + Metrics are estimates updated every 24 hours and may not accurately reflect your invoice. +

    +
    diff --git a/src/routes/console/project-[project]/settings/usage/[[invoice]]/+page.ts b/src/routes/console/project-[project]/settings/usage/[[invoice]]/+page.ts new file mode 100644 index 000000000..d29b1e799 --- /dev/null +++ b/src/routes/console/project-[project]/settings/usage/[[invoice]]/+page.ts @@ -0,0 +1,51 @@ +import { getSdkForProject, sdk } from '$lib/stores/sdk'; +import { Query } from '@appwrite.io/console'; +import type { PageLoad } from './$types'; +import type { Organization } from '$lib/stores/organization'; +import type { Aggregation, Invoice } from '$lib/sdk/billing'; +import { accumulateUsage } from '$lib/sdk/usage'; + +export const load: PageLoad = async ({ params, parent }) => { + const { invoice, project } = params; + const parentData = await parent(); + const org = parentData.organization as Organization; + + let startDate: string = org.billingCurrentInvoiceDate; + let endDate: string = org.billingNextInvoiceDate; + let currentInvoice: Invoice = undefined; + let currentAggregation: Aggregation = undefined; + + if (invoice) { + currentInvoice = await sdk.forConsole.billing.getInvoice(org.$id, invoice); + currentAggregation = await sdk.forConsole.billing.getAggregation( + org.$id, + currentInvoice.aggregationId + ); + + startDate = currentInvoice.from; + endDate = currentInvoice.to; + } + + const [invoices, usage] = await Promise.all([ + sdk.forConsole.billing.listInvoices(org.$id, [Query.orderDesc('from')]), + /** + * Workaround because project id might not be populated yet. + */ + getSdkForProject(project).project.getUsage(startDate, endDate) + ]); + + if (invoice) { + usage.usersTotal = currentAggregation.usageUsers; + usage.executionsTotal = currentAggregation.usageExecutions; + usage.filesStorageTotal = currentAggregation.usageStorage; + } + + usage.users = accumulateUsage(usage.users, usage.usersTotal); + + return { + usage, + invoices, + currentInvoice, + currentAggregation + }; +}; diff --git a/src/routes/console/project-[project]/settings/webhooks/+page.svelte b/src/routes/console/project-[project]/settings/webhooks/+page.svelte index 24c544917..d20533a1f 100644 --- a/src/routes/console/project-[project]/settings/webhooks/+page.svelte +++ b/src/routes/console/project-[project]/settings/webhooks/+page.svelte @@ -8,7 +8,6 @@ import { base } from '$app/paths'; import { page } from '$app/stores'; import { Empty } from '$lib/components'; - import { Button } from '$lib/elements/forms'; import { Pill } from '$lib/elements'; import { TableBody, @@ -19,11 +18,10 @@ TableHeader, TableScroll } from '$lib/elements/table'; - import { Container } from '$lib/layout'; + import { Container, ContainerHeader } from '$lib/layout'; import { wizard } from '$lib/stores/wizard'; import type { PageData } from './$types'; import Create from './createWebhook.svelte'; - import Heading from '$lib/components/heading.svelte'; import { updateCommandGroupRanks } from '$lib/commandCenter'; export let data: PageData; @@ -42,12 +40,12 @@ -
    - Webhooks - -
    + {#if data.webhooks.total} @@ -56,7 +54,7 @@ POST URL Events - + {#each data.webhooks.webhooks as webhook} diff --git a/src/routes/console/project-[project]/settings/webhooks/[webhook]/regenerate.svelte b/src/routes/console/project-[project]/settings/webhooks/[webhook]/regenerate.svelte index 360a2414d..a57746991 100644 --- a/src/routes/console/project-[project]/settings/webhooks/[webhook]/regenerate.svelte +++ b/src/routes/console/project-[project]/settings/webhooks/[webhook]/regenerate.svelte @@ -35,7 +35,7 @@

    Are you sure you want to generate a new Signature Key? - You will not be able to recover your current key. + You will not be able to recover the current key.

    diff --git a/src/routes/console/project-[project]/storage/+page.svelte b/src/routes/console/project-[project]/storage/+page.svelte index 1caa9b322..7515a7963 100644 --- a/src/routes/console/project-[project]/storage/+page.svelte +++ b/src/routes/console/project-[project]/storage/+page.svelte @@ -4,24 +4,16 @@ -
    - Buckets - - -
    - + ($showCreateBucket = true)} /> {#if data.buckets.total} ($showCreateBucket = true)}> {#each data.buckets.buckets as bucket} diff --git a/src/routes/console/project-[project]/storage/bucket-[bucket]/+page.svelte b/src/routes/console/project-[project]/storage/bucket-[bucket]/+page.svelte index f8732c02d..d02d6dd5c 100644 --- a/src/routes/console/project-[project]/storage/bucket-[bucket]/+page.svelte +++ b/src/routes/console/project-[project]/storage/bucket-[bucket]/+page.svelte @@ -1,52 +1,51 @@ - - - + + + +
    + +
    +
    +
    + + {#if tier === 'Starter'} +

    + You are limited to {limit} GB of storage on the {tier} plan. + + for addtional storage. +

    + {:else} +

    + You are limited to {limit} GB storage on the {tier} plan. After this amount + + . +

    + {/if} +
    +
    {#if data.files.total} @@ -200,7 +229,7 @@ {:else if data.search}
    - Sorry, we couldn't find ‘{data.search}' + Sorry, we couldn't find '{data.search}'

    There are no files that match your search.

    @@ -222,11 +251,10 @@ single href="https://appwrite.io/docs/products/storage/upload-download" target="file" - on:click={() => ($showCreate = true)} /> + on:click={() => wizard.start(Create)} /> {/if} - {#if selectedFile} - + {/if} diff --git a/src/routes/console/project-[project]/storage/bucket-[bucket]/+page.ts b/src/routes/console/project-[project]/storage/bucket-[bucket]/+page.ts index 203ea2410..13eb6f5fe 100644 --- a/src/routes/console/project-[project]/storage/bucket-[bucket]/+page.ts +++ b/src/routes/console/project-[project]/storage/bucket-[bucket]/+page.ts @@ -3,13 +3,20 @@ import { sdk } from '$lib/stores/sdk'; import { getLimit, getPage, getSearch, pageToOffset } from '$lib/helpers/load'; import { Dependencies, PAGE_LIMIT } from '$lib/constants'; import type { PageLoad } from './$types'; +import { isCloud } from '$lib/system'; +import type { OrganizationUsage } from '$lib/sdk/billing'; -export const load: PageLoad = async ({ params, depends, url, route }) => { +export const load: PageLoad = async ({ params, depends, url, route, parent }) => { + const { organization } = await parent(); depends(Dependencies.FILES); const page = getPage(url); const search = getSearch(url); const limit = getLimit(url, route, PAGE_LIMIT); const offset = pageToOffset(page, limit); + let organizationUsage: OrganizationUsage = null; + if (isCloud && organization?.$id) { + organizationUsage = await sdk.forConsole.billing.listUsage(organization.$id); + } return { offset, @@ -19,6 +26,7 @@ export const load: PageLoad = async ({ params, depends, url, route }) => { params.bucket, [Query.limit(limit), Query.offset(offset), Query.orderDesc('')], search - ) + ), + organizationUsage }; }; diff --git a/src/routes/console/project-[project]/storage/bucket-[bucket]/create-file/create.svelte b/src/routes/console/project-[project]/storage/bucket-[bucket]/create-file/create.svelte new file mode 100644 index 000000000..99c989561 --- /dev/null +++ b/src/routes/console/project-[project]/storage/bucket-[bucket]/create-file/create.svelte @@ -0,0 +1,60 @@ + + + diff --git a/src/routes/console/project-[project]/storage/bucket-[bucket]/create-file/step1.svelte b/src/routes/console/project-[project]/storage/bucket-[bucket]/create-file/step1.svelte new file mode 100644 index 000000000..8453a8ee0 --- /dev/null +++ b/src/routes/console/project-[project]/storage/bucket-[bucket]/create-file/step1.svelte @@ -0,0 +1,77 @@ + + + + Create file + Upload a file to add it to your bucket. + + {#if isCloud} + {@const size = humanFileSize(sizeToBytes(service, 'MB', 1000))} + {@const plan = tierToPlan($organization?.billingPlan)} + +

    + The {plan.name} plan has a maximum upload file size limit of {Math.floor( + parseInt(size.value) + )}{size.unit}. + {#if $organization?.billingPlan === BillingPlan.STARTER} + Upgrade to allow files of a larger size. + {/if} +

    + + {#if $organization?.billingPlan === BillingPlan.STARTER} +
    + +
    + {/if} +
    +
    + {/if} + + +
    + +
    + + {#if !showCustomId} +
    + (showCustomId = !showCustomId)}> + +
    + {:else} +
    + +
    + {/if} +
    +
    + + diff --git a/src/routes/console/project-[project]/storage/bucket-[bucket]/create-file/step2.svelte b/src/routes/console/project-[project]/storage/bucket-[bucket]/create-file/step2.svelte new file mode 100644 index 000000000..6a98039ef --- /dev/null +++ b/src/routes/console/project-[project]/storage/bucket-[bucket]/create-file/step2.svelte @@ -0,0 +1,39 @@ + + + + Update Permissions + + Choose who can access your buckets and files. For more information, check out the + + Permissions Guide + . + + {#if $bucket.fileSecurity} +
    + + File security enabled + Users will be able to access this file if they have been granted + either File or Bucket permissions. + +
    +
    + +
    + {:else} + + File security disabled + If you want to assign file permissions, navigate to Bucket settings and enable file security. + Otherwise, only Bucket permissions will be used. + + {/if} +
    diff --git a/src/routes/console/project-[project]/storage/bucket-[bucket]/create-file/store.ts b/src/routes/console/project-[project]/storage/bucket-[bucket]/create-file/store.ts new file mode 100644 index 000000000..6a6eb9248 --- /dev/null +++ b/src/routes/console/project-[project]/storage/bucket-[bucket]/create-file/store.ts @@ -0,0 +1,24 @@ +import { writable } from 'svelte/store'; + +type CreateFile = { + files: FileList | null; + id: string | null; + permissions: string[]; +}; + +const initialState: CreateFile = { + files: null, + id: null, + permissions: [] +}; + +export const createFile = (function initialize() { + const store = writable({ ...initialState }); + + return { + ...store, + reset() { + store.set({ ...initialState }); + } + }; +})(); diff --git a/src/routes/console/project-[project]/storage/bucket-[bucket]/settings/+page.svelte b/src/routes/console/project-[project]/storage/bucket-[bucket]/settings/+page.svelte index d93111fea..e229e256c 100644 --- a/src/routes/console/project-[project]/storage/bucket-[bucket]/settings/+page.svelte +++ b/src/routes/console/project-[project]/storage/bucket-[bucket]/settings/+page.svelte @@ -61,7 +61,9 @@ import { Button, Form, + FormItem, FormList, + InputChoice, InputSelect, InputSwitch, InputTags, @@ -78,6 +80,8 @@ import Delete from '../deleteBucket.svelte'; import { bucket } from '../store'; import UpdateMaxFileSize from './updateMaxFileSize.svelte'; + import { readOnly } from '$lib/stores/billing'; + import { GRACE_PERIOD_OVERRIDE } from '$lib/system'; let showDelete = false; @@ -229,7 +233,12 @@ - + @@ -243,13 +252,17 @@ id="name" label="Name" placeholder="Enter name" - autocomplete={false} + readonly={$readOnly && !GRACE_PERIOD_OVERRIDE} bind:value={bucketName} /> - @@ -300,7 +313,10 @@

    - @@ -316,54 +332,29 @@

    -
  • - -
  • -
  • - -
  • - -
  • + + + This parameter allows you to configure whether or not the files + inside the bucket should be scanned by the Appwrite Antivirus + scanner. + + @@ -405,7 +396,7 @@
    - File Extensions + Allowed file Extensions

    Allowed file extensions. A maximum of 100 file extensions can be added. Leave blank to allow all file types. @@ -438,7 +429,9 @@ - + diff --git a/src/routes/console/project-[project]/storage/bucket-[bucket]/settings/updateMaxFileSize.svelte b/src/routes/console/project-[project]/storage/bucket-[bucket]/settings/updateMaxFileSize.svelte index 2c6a5be45..501154834 100644 --- a/src/routes/console/project-[project]/storage/bucket-[bucket]/settings/updateMaxFileSize.svelte +++ b/src/routes/console/project-[project]/storage/bucket-[bucket]/settings/updateMaxFileSize.svelte @@ -1,11 +1,19 @@ + + + +

    Contact the Appwrite Team

    +

    + If you found a bug or have questions, please reach out to the Appwrite team. We try to + respond to all messages within our office hours. +

    +

    Available: Mon-Fri 09:00 - 17:00 UTC

    +
    + Currently: + {#if isSupportOnline()} +
    + + diff --git a/src/routes/console/wizard/cloudOrganization/addressDetails.svelte b/src/routes/console/wizard/cloudOrganization/addressDetails.svelte new file mode 100644 index 000000000..608833f9c --- /dev/null +++ b/src/routes/console/wizard/cloudOrganization/addressDetails.svelte @@ -0,0 +1,141 @@ + + + + Billing address + Add a billing address for your organization. + + + {#if addressList} + + +
    +

    {element.streetAddress}

    + {#if element?.addressLine2} +

    {element.addressLine2}

    + {/if} +

    {element.city}

    +

    {element.state}

    +

    {element.postalCode}

    +

    {element.country}

    +
    +
    + + + Add new billing address + + + + + + + + + + + + + +
    + {/if} +
    +
    diff --git a/src/routes/console/wizard/cloudOrganization/confirmDetails.svelte b/src/routes/console/wizard/cloudOrganization/confirmDetails.svelte new file mode 100644 index 000000000..58aedf179 --- /dev/null +++ b/src/routes/console/wizard/cloudOrganization/confirmDetails.svelte @@ -0,0 +1,104 @@ + + + + Confirm your details + + Confirm the details of your new organization and start your free trial. + + +

    Organization name

    +
    +

    {$createOrganization.name}

    + {#if $createOrganization?.id} + {$createOrganization.id} + {/if} +
    + + {#if $createOrganization.billingPlan !== BillingPlan.STARTER} +
    +

    Additional members

    +

    {collaboratorsNumber} members

    +
    +
    +

    Payment

    + {#await fetchCard()} +
    +
    +
    + {:then card} + +

    + {card?.brand} ending in {card?.last4} +

    + +
    + {/await} +
    + {/if} + + {#if $createOrganization.billingPlan !== BillingPlan.STARTER} + +

    {plan.name} plan

    +

    ${plan.price}

    +
    + +

    Additional members ({collaboratorsNumber})

    +

    ${collaboratorPrice * collaboratorsNumber}

    +
    +
    + {/if} + +

    Estimated total

    +

    ${totalExpences}

    +
    + + {#if $createOrganization.billingPlan !== BillingPlan.STARTER} +

    + This amount, and any additional usage fees, will be charged on a recurring 30-day + billing cycle after your trial period ends on {toLocaleDate(billingPayDate.toString())}. +

    + {/if} + + diff --git a/src/routes/console/wizard/cloudOrganization/inviteMembers.svelte b/src/routes/console/wizard/cloudOrganization/inviteMembers.svelte new file mode 100644 index 000000000..3c84e8c81 --- /dev/null +++ b/src/routes/console/wizard/cloudOrganization/inviteMembers.svelte @@ -0,0 +1,102 @@ + + + + Invite organization members + + Invite team members to collaborate with you in the Appwrite console. Members will have + access to all services and payment data within your organization. + + + + {#if $createOrganization.billingPlan === BillingPlan.SCALE} + You can add unlimited organization members on the {plan.name} plan at no cost. Each member + added will receive an email invite to your organization on completion. + {:else if $createOrganization.billingPlan === BillingPlan.PRO} + You can add unlimited organization members on the {plan.name} plan for + ${plan.addons.member.price} each per month. Each member added will receive an + email invite to your organization on completion. + {/if} + + +
    +
    + + + + + + +
    + + {#if $createOrganization?.collaborators?.length} +
    +
  • + + Collaborator + {#if $createOrganization.billingPlan === BillingPlan.PRO} + Cost + {/if} + + + + {#each $createOrganization.collaborators as collaborator} + + {collaborator} + {#if $createOrganization.billingPlan === BillingPlan.PRO} + 15$ + {/if} + + + + + {/each} + +
    + + {/if} + diff --git a/src/routes/console/wizard/cloudOrganization/organizationDetails.svelte b/src/routes/console/wizard/cloudOrganization/organizationDetails.svelte new file mode 100644 index 000000000..90db8ad77 --- /dev/null +++ b/src/routes/console/wizard/cloudOrganization/organizationDetails.svelte @@ -0,0 +1,139 @@ + + + + Organization details + + + + {#if !showCustomId} +
    + (showCustomId = !showCustomId)}> + +
    + {:else} + + {/if} +
    + +

    Plan

    +

    + For more details on our plans, visit our pricing page. +

    +
      +
    • + + +
      +

      + {tierFree.name} - ${freePlan?.price}/month +

      +

      {tierFree.description}

      +
      +
      +
      +
    • + +
    • + + +
      +

      + {tierPro.name} - ${proPlan.price}/month per organization member + exta + usage +

      +

      + {tierPro.description} +

      +
      + 14 DAY FREE TRIAL +
      +
      +
    • +
    • + + +
      +

      + {tierScale.name} - ${scalePlan.price}/month + extra usage +

      +

      + {tierScale.description} +

      +
      +
      + + COMING SOON +
      +
      +
      +
    • +
    +
    diff --git a/src/routes/console/wizard/cloudOrganization/paymentDetails.svelte b/src/routes/console/wizard/cloudOrganization/paymentDetails.svelte new file mode 100644 index 000000000..b9378f5f1 --- /dev/null +++ b/src/routes/console/wizard/cloudOrganization/paymentDetails.svelte @@ -0,0 +1,115 @@ + + + + Payment details + + Add a payment method to your organization. You will not be charged until your trial ends on {toLocaleDate(billingPayDate.toString())}. + + + + + + +

    + Restrict your resource usage by setting a budget cap. . +

    + {#if budgetEnabled} +
    + +
    + {/if} +
    +
    +
    diff --git a/src/routes/console/wizard/cloudOrganization/store.ts b/src/routes/console/wizard/cloudOrganization/store.ts new file mode 100644 index 000000000..0c2e71e7a --- /dev/null +++ b/src/routes/console/wizard/cloudOrganization/store.ts @@ -0,0 +1,26 @@ +import { BillingPlan } from '$lib/constants'; +import type { WizardStepsType } from '$lib/layout/wizard.svelte'; +import type { Tier } from '$lib/stores/billing'; +import { writable } from 'svelte/store'; + +export const createOrgSteps = writable(new Map()); +export const createOrganizationFinalAction = writable('Start trial'); + +export const createOrganization = writable<{ + id?: string; + name: string; + billingPlan: Tier; + paymentMethodId: string; + billingAddressId: string; + collaborators?: string[]; + billingBudget?: number; + taxId?: string; +}>({ + id: null, + name: null, + billingPlan: BillingPlan.PRO, + paymentMethodId: null, + collaborators: [], + billingAddressId: null, + taxId: null +}); diff --git a/src/routes/console/wizard/cloudOrganization/usageRates.svelte b/src/routes/console/wizard/cloudOrganization/usageRates.svelte new file mode 100644 index 000000000..e90faac66 --- /dev/null +++ b/src/routes/console/wizard/cloudOrganization/usageRates.svelte @@ -0,0 +1,116 @@ + + + + {#if isFree} + Usage on the Starter plan is limited for the following resources. Next billing period: {toLocaleDate( + nextDate + )}. + {:else if tier === BillingPlan.PRO} +

    + Usage on the Pro plan will be charged at the end of each billing period at the following + rates. Next billing period: {toLocaleDate(nextDate)}. +

    + {:else if tier === BillingPlan.SCALE} +

    + Usage on the Scale plan will be charged at the end of each billing period at the + following rates. Next billing period: {toLocaleDate(nextDate)}. +

    + {/if} + + + Resource + Limit + {#if !isFree} + Rate + {/if} + + + {#each planData as usage} + {#if usage['id'] === 'members'} + + {usage.resource} + + {plan[usage.id] || 'Unlimited'} + + {#if !isFree} + + ${plan.addons.member.price}/{usage?.unit} + + {/if} + + {:else} + {@const addon = plan.addons[usage.id]} + + {usage.resource} + + {abbreviateNumber(plan[usage.id])}{usage?.unit} + + {#if !isFree} + + ${addon?.price}/{['MB', 'GB', 'TB'].includes(addon?.unit) + ? addon?.value + : abbreviateNumber(addon?.value, 0)}{usage?.unit} + + {/if} + + {/if} + {/each} + +
    + + + +
    diff --git a/src/routes/console/wizard/cloudOrganizationChangeTier/addressDetails.svelte b/src/routes/console/wizard/cloudOrganizationChangeTier/addressDetails.svelte new file mode 100644 index 000000000..5bd9ebaf5 --- /dev/null +++ b/src/routes/console/wizard/cloudOrganizationChangeTier/addressDetails.svelte @@ -0,0 +1,144 @@ + + + + Billing address + Add a billing address for your organization. + + + {#if addressList} + + +
    +

    {element.streetAddress}

    + {#if element?.addressLine2} +

    {element.addressLine2}

    + {/if} +

    {element.city}

    +

    {element.state}

    +

    {element.postalCode}

    +

    {element.country}

    +
    +
    + + + Add new billing address + + + + + + + + + + + + + +
    + {/if} +
    +
    diff --git a/src/routes/console/wizard/cloudOrganizationChangeTier/choosePlan.svelte b/src/routes/console/wizard/cloudOrganizationChangeTier/choosePlan.svelte new file mode 100644 index 000000000..76ff8498e --- /dev/null +++ b/src/routes/console/wizard/cloudOrganizationChangeTier/choosePlan.svelte @@ -0,0 +1,194 @@ + + + + Choose a plan + +

    Plan

    +

    + For more details on our plans, visit our pricing page. +

    +
      +
    • + + +
      +

      + {tierFree.name} - ${freePlan.price}/month +

      +

      {tierFree.description}

      +
      +
      + {#if $organization.billingPlan === BillingPlan.STARTER} + CURRENT PLAN + {/if} +
      +
      +
      +
    • +
    • + + +
      +

      + {tierPro.name} - ${proPlan.price}/month per organization member + extra + usage +

      +

      + {tierPro.description} +

      +
      +
      + {#if $organization.billingPlan === BillingPlan.PRO} + CURRENT PLAN + {:else} + 14 DAY FREE TRIAL + {/if} +
      +
      +
      +
    • +
    • + + +
      +

      + {tierScale.name} - ${scalePlan.price}/month + extra usage +

      +

      + {tierScale.description} +

      +
      +
      + COMING SOON +
      +
      +
      +
    • +
    +
    diff --git a/src/routes/console/wizard/cloudOrganizationChangeTier/confirmDetails.svelte b/src/routes/console/wizard/cloudOrganizationChangeTier/confirmDetails.svelte new file mode 100644 index 000000000..e3c33693b --- /dev/null +++ b/src/routes/console/wizard/cloudOrganizationChangeTier/confirmDetails.svelte @@ -0,0 +1,140 @@ + + +{#if downgradeToStarter} + + Confirm plan change + + Your feedback is important to us and helps us improve the services Appwrite offer. If + there is a specific reason you chose to change your plan at this time, please let us + know. + + + + + +{:else} + + Confirm your details + + Confirm the details of your new organization and start your free trial. + + +

    Organization name

    +
    +

    {$organization.name}

    +
    + + {#if $changeOrganizationTier.billingPlan !== BillingPlan.STARTER} +
    +

    Additional members

    +

    {collaboratorsNumber} members

    +
    +
    +

    Payment

    + {#await fetchCard()} +
    +
    +
    + {:then card} + +

    + Card ending in {card.last4} +

    + +
    + {/await} +
    + {/if} + + + ($changeOrganizationTier.couponCode = e.detail.code)} /> + {#if $changeOrganizationTier.billingPlan !== BillingPlan.STARTER} + +

    {plan.name} plan

    +

    ${plan.price}

    +
    + +

    Additional members ({collaboratorsNumber})

    +

    ${collaboratorPrice * collaboratorsNumber}

    +
    + {#if couponData?.status === 'active'} + +

    Credits applied ({couponData.credits})

    +

    -${couponData.credits}

    +
    + {/if} + {/if} +
    + +

    Estimated total

    +

    + ${couponData?.status === 'active' + ? totalExpences - couponData.credits >= 0 + ? totalExpences - couponData.credits + : 0 + : totalExpences} +

    +
    + +

    + This amount, and any additional usage fees, will be charged on a recurring 30-day + billing cycle after your trial period ends on {toLocaleDate(billingPayDate.toString())}. +

    + + +{/if} diff --git a/src/routes/console/wizard/cloudOrganizationChangeTier/inviteMembers.svelte b/src/routes/console/wizard/cloudOrganizationChangeTier/inviteMembers.svelte new file mode 100644 index 000000000..32e30c8b3 --- /dev/null +++ b/src/routes/console/wizard/cloudOrganizationChangeTier/inviteMembers.svelte @@ -0,0 +1,118 @@ + + + + Invite organization members + + Invite team members to collaborate with you in the Appwrite console. Members will have + access to all services and payment data within your organization. + + + + {#if $changeOrganizationTier.billingPlan === BillingPlan.SCALE} + You can add unlimited organization members on the {plan.name} plan at no cost. Each member + added will receive an email invite to your organization on completion. + {:else if $changeOrganizationTier.billingPlan === BillingPlan.PRO} + You can add unlimited organization members on the {plan.name} plan for + ${plan.addons.member.price} each per month. Each member added will receive an + email invite to your organization on completion. + {/if} + + +
    +
    + + + + + +
    +
    + + {#if $changeOrganizationTier?.collaborators?.length} +
    + + + Collaborator + {#if $changeOrganizationTier.billingPlan === BillingPlan.PRO} + Cost + {/if} + + + + {#each $changeOrganizationTier.collaborators as collaborator} + + {collaborator} + {#if $changeOrganizationTier.billingPlan === BillingPlan.PRO} + 15$ + {/if} + + + + + {/each} + +
    +
    + {/if} +
    diff --git a/src/routes/console/wizard/cloudOrganizationChangeTier/paymentDetails.svelte b/src/routes/console/wizard/cloudOrganizationChangeTier/paymentDetails.svelte new file mode 100644 index 000000000..b82943719 --- /dev/null +++ b/src/routes/console/wizard/cloudOrganizationChangeTier/paymentDetails.svelte @@ -0,0 +1,124 @@ + + + + Payment details + + Confirm the payment method for your organization. + + + + + + +

    + Restrict your resource usage by setting a budget cap. . +

    + {#if budgetEnabled} +
    + +
    + {/if} +
    +
    +
    diff --git a/src/routes/console/wizard/cloudOrganizationChangeTier/planExcess.svelte b/src/routes/console/wizard/cloudOrganizationChangeTier/planExcess.svelte new file mode 100644 index 000000000..bfb737582 --- /dev/null +++ b/src/routes/console/wizard/cloudOrganizationChangeTier/planExcess.svelte @@ -0,0 +1,145 @@ + + + + + {#if currentTier === BillingPlan.STARTER} + Your usage exceeds the {plan.name} plan limits + {:else} + Changing your plan now will result in removal of organization members and more + {/if} + + + {#if excess?.members > 0} + {#if currentTier === BillingPlan.STARTER} + The Starter plan has a limit of one organization member. By proceeding, all but the + creator of the organization admin will be removed. + {:else if currentTier === BillingPlan.PRO} + Additional organization members on the Pro plan cost {collaboratorPrice} per member per billing + period. By proceeding, you acknowledge your fees may increase in your next billing period. + {/if} + {/if} + {#if excess?.bandwidth > 0 || excess?.executions > 0 || excess?.storage > 0 || excess?.users} + Until you reduce your usage, you will be unable to add to the resources listed below in all + projects within your organization. The current billing cycle will end on {toLocaleDate( + $organization.billingCurrentInvoiceDate + )}. Any executions, bandwidth, or messaging usage will be reset at that time. + {/if} + + + {#if currentTier === BillingPlan.STARTER} + + {:else if currentTier === BillingPlan.PRO} + + {/if} + + + + + + Resource + Free limit + Excess usage + + + {#if excess?.members} + + Organization members + {plan.members} members + +

    + + {excess?.members} members +

    +
    +
    + {/if} + {#if excess?.storage} + + Storage + {plan.storage} GB + +

    + + {humanFileSize(excess?.storage)} +

    +
    +
    + {/if} + {#if excess?.executions} + + Function executions + + {abbreviateNumber(plan.executions)} executions + + +

    + + + {formatNum(excess?.executions)} executions + +

    +
    +
    + {/if} + {#if excess?.users} + + Users + + {abbreviateNumber(plan.users)} users + + +

    + + + {formatNum(excess?.users)} users + +

    +
    +
    + {/if} +
    +
    diff --git a/src/routes/console/wizard/cloudOrganizationChangeTier/store.ts b/src/routes/console/wizard/cloudOrganizationChangeTier/store.ts new file mode 100644 index 000000000..f9f97b578 --- /dev/null +++ b/src/routes/console/wizard/cloudOrganizationChangeTier/store.ts @@ -0,0 +1,34 @@ +import { BillingPlan } from '$lib/constants'; +import type { WizardStepsType } from '$lib/layout/wizard.svelte'; +import type { Tier } from '$lib/stores/billing'; +import { writable } from 'svelte/store'; + +export const changeTierSteps = writable(new Map()); +export const isUpgrade = writable(false); +export const changeOrganizationFinalAction = writable('Start trial'); + +export const changeOrganizationTier = writable<{ + billingPlan: Tier; + paymentMethodId: string; + billingAddressId: string; + billingBudget?: number; + collaborators?: string[]; + isOverLimit?: boolean; + limitOverflow?: { + bandwidth?: number; + executions?: number; + storage?: number; + users?: number; + members?: number; + }; + taxId?: string; + feedbackMessage?: string; + couponCode?: string; +}>({ + billingPlan: BillingPlan.PRO, + paymentMethodId: null, + collaborators: [], + isOverLimit: false, + billingAddressId: null, + taxId: null +}); diff --git a/src/routes/console/wizard/cloudOrganizationChangeTier/usageExcess.svelte b/src/routes/console/wizard/cloudOrganizationChangeTier/usageExcess.svelte new file mode 100644 index 000000000..f3b1a6baa --- /dev/null +++ b/src/routes/console/wizard/cloudOrganizationChangeTier/usageExcess.svelte @@ -0,0 +1,20 @@ + + + + Your usage exceeds the limits of the {tierToPlan($organization.billingPlan).name} plan + + View the current usage for {$organization.name} and where you will exceed the limits of + your new plan. + + + + diff --git a/src/routes/console/wizardFeedback/step1.svelte b/src/routes/console/wizard/feedback/step1.svelte similarity index 100% rename from src/routes/console/wizardFeedback/step1.svelte rename to src/routes/console/wizard/feedback/step1.svelte diff --git a/src/routes/console/wizardFeedback/step2.svelte b/src/routes/console/wizard/feedback/step2.svelte similarity index 100% rename from src/routes/console/wizardFeedback/step2.svelte rename to src/routes/console/wizard/feedback/step2.svelte diff --git a/src/routes/console/wizard/support/mobileSupportModal.svelte b/src/routes/console/wizard/support/mobileSupportModal.svelte new file mode 100644 index 000000000..31f2108c6 --- /dev/null +++ b/src/routes/console/wizard/support/mobileSupportModal.svelte @@ -0,0 +1,10 @@ + + + + + diff --git a/src/routes/console/wizard/support/step1.svelte b/src/routes/console/wizard/support/step1.svelte new file mode 100644 index 000000000..06f8f265b --- /dev/null +++ b/src/routes/console/wizard/support/step1.svelte @@ -0,0 +1,61 @@ + + + + How can we help you? + + Please describe your request in detail. If applicable, include steps for reproduction of any + in-app issues. + +
    +

    Choose a topic

    +
    + {#each ['general', 'billing', 'technical'] as topic} + { + $supportData.category = topic; + }}>{topic} + {/each} +
    +
    +
    + + {#if options?.length} + + {/if} + + +
    +
    diff --git a/src/routes/console/wizard/support/store.ts b/src/routes/console/wizard/support/store.ts new file mode 100644 index 000000000..a447e1d4d --- /dev/null +++ b/src/routes/console/wizard/support/store.ts @@ -0,0 +1,32 @@ +import { writable } from 'svelte/store'; + +export type SupportData = { + message: string; + category: string; + file?: File | null; + project?: string; +}; + +export const supportData = writable({ + message: '', + category: 'general', + file: null +}); + +export function isSupportOnline() { + const currentDate = new Date(); + const day = currentDate.getUTCDay(); + const hour = currentDate.getUTCHours(); + + if (day === 1 || day === 6) { + return false; + } + + if (hour < 9 || hour > 17) { + return false; + } + + return true; +} + +export const showSupportModal = writable(false); diff --git a/src/routes/login/+page.svelte b/src/routes/login/+page.svelte index d0185d2d2..b88a2da85 100644 --- a/src/routes/login/+page.svelte +++ b/src/routes/login/+page.svelte @@ -15,6 +15,7 @@ import { Dependencies } from '$lib/constants'; import { Submit, trackEvent, trackError } from '$lib/actions/analytics'; import { isCloud } from '$lib/system'; + import { page } from '$app/stores'; let mail: string, pass: string, disabled: boolean; @@ -28,7 +29,17 @@ message: 'Successfully logged in.' }); trackEvent(Submit.AccountCreate); - await goto(`${base}/console`); + if ($page.url.searchParams) { + const redirect = $page.url.searchParams.get('redirect'); + $page.url.searchParams.delete('redirect'); + if (redirect) { + await goto(`${base}${redirect}${$page.url.search}`); + } else { + await goto(`${base}/console${$page.url.search ?? ''}`); + } + } else { + await goto(`${base}/console`); + } } catch (error) { disabled = false; addNotification({ @@ -40,9 +51,19 @@ } function onGithubLogin() { + let url = window.location.origin; + if ($page.url.searchParams) { + const redirect = $page.url.searchParams.get('redirect'); + $page.url.searchParams.delete('redirect'); + if (redirect) { + url = `${base}${redirect}${$page.url.search}`; + } else { + url = `${base}/console${$page.url.search ?? ''}`; + } + } sdk.forConsole.account.createOAuth2Session( 'github', - window.location.origin, + window.location.origin + url, window.location.origin, ['read:user', 'user:email'] ); @@ -110,7 +131,9 @@ Forgot Password? diff --git a/src/routes/register/+page.svelte b/src/routes/register/+page.svelte index 3677e7de3..f2c15e4f3 100644 --- a/src/routes/register/+page.svelte +++ b/src/routes/register/+page.svelte @@ -18,6 +18,7 @@ import { Submit, trackError, trackEvent } from '$lib/actions/analytics'; import { ID } from '@appwrite.io/console'; import { isCloud } from '$lib/system'; + import { page } from '$app/stores'; let name: string, mail: string, pass: string, disabled: boolean; let terms = false; @@ -28,7 +29,17 @@ await sdk.forConsole.account.create(ID.unique(), mail, pass, name ?? ''); await sdk.forConsole.account.createEmailSession(mail, pass); await invalidate(Dependencies.ACCOUNT); - await goto(`${base}/console`); + if ($page.url.searchParams) { + const redirect = $page.url.searchParams.get('redirect'); + $page.url.searchParams.delete('redirect'); + if (redirect) { + await goto(`${base}${redirect}${$page.url.search}`); + } else { + await goto(`${base}/console${$page.url.search ?? ''}`); + } + } else { + await goto(`${base}/console`); + } trackEvent(Submit.AccountCreate); } catch (error) { disabled = false; diff --git a/src/routes/register/invite/[slug]/+page.svelte b/src/routes/register/invite/[slug]/+page.svelte index 0d31350a4..fa7cacb81 100644 --- a/src/routes/register/invite/[slug]/+page.svelte +++ b/src/routes/register/invite/[slug]/+page.svelte @@ -73,7 +73,9 @@ throw new Error((await res.json()).message); } else { await sdk.forConsole.account.createEmailSession(mail, pass); - await sdk.forConsole.account.updatePrefs({ code }); + const prefs = await sdk.forConsole.account.getPrefs(); + const newPrefs = { ...prefs, code }; + await sdk.forConsole.account.updatePrefs(newPrefs); await invalidate(Dependencies.ACCOUNT); await goto(`${base}/console`); trackEvent('submit_account_create', { code: code }); diff --git a/src/routes/store.ts b/src/routes/store.ts index be6859877..5c5fe880d 100644 --- a/src/routes/store.ts +++ b/src/routes/store.ts @@ -1,10 +1,5 @@ -import { page } from '$app/stores'; -import type { Models } from '@appwrite.io/console'; -import { derived, writable } from 'svelte/store'; +import { writable } from 'svelte/store'; export const loading = writable(true); -export const organizations = derived(page, ($page) => { - return $page.data.organizations as Models.TeamList; -}); export const requestedMigration = writable | null>(null); diff --git a/static/legal/dpa.pdf b/static/legal/dpa.pdf new file mode 100644 index 000000000..0f69cf634 Binary files /dev/null and b/static/legal/dpa.pdf differ diff --git a/svelte.config.js b/svelte.config.js index ca8f1dd0b..a3e96612f 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -1,11 +1,13 @@ import adapter from '@sveltejs/adapter-static'; import preprocess from 'svelte-preprocess'; +import { preprocessMeltUI } from '@melt-ui/pp'; +import sequence from 'svelte-sequential-preprocessor'; /** @type {import('@sveltejs/kit').Config} */ const config = { // Consult https://github.com/sveltejs/svelte-preprocess // for more information about preprocessors - preprocess: preprocess(), + preprocess: sequence([preprocess(), preprocessMeltUI()]), compilerOptions: { accessors: process.env.VITEST },