From 5fe29af51a8a866fa89a549f1f0962612de82cf7 Mon Sep 17 00:00:00 2001 From: Abdullah Atta Date: Tue, 1 Jul 2025 08:54:04 +0500 Subject: [PATCH] web: migrate to paddle billing + new plans --- apps/web/package-lock.json | 1374 ++++++++++++++++- apps/web/package.json | 1 + apps/web/src/bootstrap.tsx | 6 +- apps/web/src/common/db.ts | 23 +- .../src/components/navigation-menu/index.tsx | 2 + .../web/src/dialogs/buy-dialog/buy-dialog.tsx | 107 +- apps/web/src/dialogs/buy-dialog/helpers.ts | 11 + apps/web/src/dialogs/buy-dialog/paddle.tsx | 634 ++++---- apps/web/src/dialogs/buy-dialog/plan-list.tsx | 197 ++- apps/web/src/dialogs/buy-dialog/plans.ts | 153 +- apps/web/src/dialogs/buy-dialog/store.ts | 9 +- apps/web/src/dialogs/buy-dialog/types.ts | 164 +- .../settings/components/billing-history.tsx | 93 +- .../components/subscription-status.tsx | 206 +-- .../settings/components/user-profile.tsx | 103 +- .../dialogs/settings/subscription-settings.ts | 160 +- apps/web/src/global.d.ts | 4 + apps/web/src/hooks/use-is-user-premium.ts | 9 +- apps/web/src/views/payments.tsx | 61 + 19 files changed, 2495 insertions(+), 822 deletions(-) create mode 100644 apps/web/src/views/payments.tsx diff --git a/apps/web/package-lock.json b/apps/web/package-lock.json index 82058ae38..40ef564f2 100644 --- a/apps/web/package-lock.json +++ b/apps/web/package-lock.json @@ -34,6 +34,7 @@ "@notesnook/themes-server": "file:../../servers/themes", "@notesnook/ui": "file:../../packages/ui", "@notesnook/web-clipper": "file:../../extensions/web-clipper", + "@paddle/paddle-js": "^1.4.2", "@react-pdf-viewer/core": "^3.12.0", "@react-pdf-viewer/toolbar": "^3.12.0", "@rehookify/datepicker": "^6.6.7", @@ -547,7 +548,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -656,7 +656,6 @@ "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.5.tgz", "integrity": "sha512-SBuTAjg91A3eKOvD+bPEz3LlhHZRNu1nFOVts9lzDJTXshHTjII0BAtDS3Y2DAkdZdDKWVZGVwkDfc4Clxn1dg==", - "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", @@ -815,7 +814,6 @@ "version": "7.27.3", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.27.1", @@ -947,7 +945,6 @@ "version": "7.27.6", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", - "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.27.2", @@ -2211,6 +2208,28 @@ "react": ">=16.8.0" } }, + "node_modules/@electron/get": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", + "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, "node_modules/@emotion/babel-plugin": { "version": "11.13.5", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", @@ -3163,6 +3182,12 @@ "@otplib/plugin-thirty-two": "^12.0.1" } }, + "node_modules/@paddle/paddle-js": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@paddle/paddle-js/-/paddle-js-1.4.2.tgz", + "integrity": "sha512-pY9jYS+g3vk6ecc7GbCsVSuBEo1Yh+hxYE4i2XoY5/ZBAZLuCkQi/80hOmSBkFoaETohOpDmurILlQdgUHUcvw==", + "license": "Apache-2.0" + }, "node_modules/@playwright/test": { "version": "1.48.2", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.48.2.tgz", @@ -3741,6 +3766,19 @@ "win32" ] }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, "node_modules/@stablelib/aead": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@stablelib/aead/-/aead-1.0.1.tgz", @@ -4591,6 +4629,19 @@ "@swc/counter": "^0.1.3" } }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "license": "MIT", + "peer": true, + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@tanstack/query-core": { "version": "4.39.2", "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.39.2.tgz", @@ -4665,6 +4716,37 @@ "polished": "^4.0.5" } }, + "node_modules/@theme-ui/color-modes": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@theme-ui/color-modes/-/color-modes-0.16.2.tgz", + "integrity": "sha512-jWEWx53lxNgWCT38i/kwLV2rsvJz8lVZgi5oImnVwYba9VejXD23q1ckbNFJHosQ8KKXY87ht0KPC6BQFIiHtQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@theme-ui/core": "^0.16.2", + "@theme-ui/css": "^0.16.2", + "deepmerge": "^4.2.2" + }, + "peerDependencies": { + "@emotion/react": "^11.11.1", + "react": ">=18" + } + }, + "node_modules/@theme-ui/color-modes/node_modules/@theme-ui/core": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@theme-ui/core/-/core-0.16.2.tgz", + "integrity": "sha512-bBd/ltbwO9vIUjF1jtlOX6XN0IIOdf1vzBp2JCKsSOqdfn84m+XL8OogIe/zOhQ+aM94Nrq4+32tFJc8sFav4Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "@theme-ui/css": "^0.16.2", + "deepmerge": "^4.2.2" + }, + "peerDependencies": { + "@emotion/react": "^11.11.1", + "react": ">=18" + } + }, "node_modules/@theme-ui/components": { "version": "0.16.1", "resolved": "https://registry.npmjs.org/@theme-ui/components/-/components-0.16.1.tgz", @@ -4710,6 +4792,37 @@ "@emotion/react": "^11.11.1" } }, + "node_modules/@theme-ui/theme-provider": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@theme-ui/theme-provider/-/theme-provider-0.16.2.tgz", + "integrity": "sha512-LRnVevODcGqO0JyLJ3wht+PV3ZoZcJ7XXLJAJWDoGeII4vZcPQKwVy4Lpz/juHsZppQxKcB3U+sQDGBnP25irQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@theme-ui/color-modes": "^0.16.2", + "@theme-ui/core": "^0.16.2", + "@theme-ui/css": "^0.16.2" + }, + "peerDependencies": { + "@emotion/react": "^11.11.1", + "react": ">=18" + } + }, + "node_modules/@theme-ui/theme-provider/node_modules/@theme-ui/core": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@theme-ui/core/-/core-0.16.2.tgz", + "integrity": "sha512-bBd/ltbwO9vIUjF1jtlOX6XN0IIOdf1vzBp2JCKsSOqdfn84m+XL8OogIe/zOhQ+aM94Nrq4+32tFJc8sFav4Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "@theme-ui/css": "^0.16.2", + "deepmerge": "^4.2.2" + }, + "peerDependencies": { + "@emotion/react": "^11.11.1", + "react": ">=18" + } + }, "node_modules/@trpc/client": { "version": "10.45.2", "resolved": "https://registry.npmjs.org/@trpc/client/-/client-10.45.2.tgz", @@ -4742,7 +4855,6 @@ "version": "10.45.2", "resolved": "https://registry.npmjs.org/@trpc/server/-/server-10.45.2.tgz", "integrity": "sha512-wOrSThNNE4HUnuhJG6PfDRp4L2009KDVxsd+2VYH8ro6o/7/jwYZ8Uu5j+VaW+mOmc8EHerHzGcdbGNQSAUPgg==", - "dev": true, "funding": [ "https://trpc.io/sponsor" ], @@ -4793,6 +4905,19 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -4802,6 +4927,30 @@ "@types/ms": "*" } }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", @@ -4832,6 +4981,13 @@ "@types/unist": "^2" } }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", + "license": "MIT", + "peer": true + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -4845,6 +5001,16 @@ "integrity": "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==", "license": "MIT" }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/marked": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/@types/marked/-/marked-6.0.0.tgz", @@ -4875,7 +5041,6 @@ "version": "24.0.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.3.tgz", "integrity": "sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~7.8.0" @@ -4915,14 +5080,12 @@ "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "dev": true, "license": "MIT" }, "node_modules/@types/react": { "version": "18.3.5", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.5.tgz", "integrity": "sha512-WeqMfGJLGuLCqHGYRGHxnKrXcTitc6L/nBUWfWPcTarG3t9PsquqUMuVeXZeca+mglY4Vo5GZjCi0A3Or2lnxA==", - "dev": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -4976,6 +5139,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/styled-system": { "version": "5.1.23", "resolved": "https://registry.npmjs.org/@types/styled-system/-/styled-system-5.1.23.tgz", @@ -5005,6 +5178,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@vitejs/plugin-react-swc": { "version": "3.10.2", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.10.2.tgz", @@ -5168,6 +5352,198 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true + }, "node_modules/@zip.js/zip.js": { "version": "2.7.62", "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.7.62.tgz", @@ -5229,6 +5605,51 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/ajv-keywords": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", @@ -5627,6 +6048,15 @@ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", "license": "ISC" }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -5695,6 +6125,16 @@ "ieee754": "^1.2.1" } }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": "*" + } + }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -5718,6 +6158,35 @@ "node": ">=8" } }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "license": "MIT", + "peer": true, + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -5919,6 +6388,17 @@ "node": ">=10" } }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.0" + } + }, "node_modules/clipboard-polyfill": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/clipboard-polyfill/-/clipboard-polyfill-4.1.0.tgz", @@ -5940,6 +6420,19 @@ "node": ">=12" } }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "license": "MIT", + "peer": true, + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -6214,7 +6707,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "devOptional": true, "license": "MIT", "dependencies": { "mimic-response": "^3.1.0" @@ -6230,7 +6722,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "devOptional": true, "license": "MIT", "engines": { "node": ">=10" @@ -6268,11 +6759,21 @@ "node": ">=0.10.0" } }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", @@ -6300,7 +6801,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "define-data-property": "^1.0.1", @@ -6349,6 +6850,14 @@ "node": ">=8" } }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/diff": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", @@ -6510,6 +7019,25 @@ "node": ">=0.10.0" } }, + "node_modules/electron": { + "version": "37.1.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-37.1.0.tgz", + "integrity": "sha512-Fcr3yfAw4oU392waVZSlrFUQx4P+h/k31+PRgkBY9tFx9E/zxzdPQQj0achZlG1HRDusw3ooQB+OXb9PvufdzA==", + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@electron/get": "^2.0.0", + "@types/node": "^22.7.7", + "extract-zip": "^2.0.1" + }, + "bin": { + "electron": "cli.js" + }, + "engines": { + "node": ">= 12.20.55" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.168", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.168.tgz", @@ -6530,6 +7058,23 @@ "electron": ">19.0.0" } }, + "node_modules/electron/node_modules/@types/node": { + "version": "22.15.34", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.34.tgz", + "integrity": "sha512-8Y6E5WUupYy1Dd0II32BsWAx5MWdcnRd8L84Oys3veg1YrYtNtzgO4CFhiBg6MDSjk7Ay36HYOnU7/tuOzIzcw==", + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/electron/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT", + "peer": true + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -6551,12 +7096,26 @@ "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "devOptional": true, "license": "MIT", "dependencies": { "once": "^1.4.0" } }, + "node_modules/enhanced-resolve": { + "version": "5.18.2", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz", + "integrity": "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -6569,6 +7128,16 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -6717,6 +7286,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/esbuild": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", @@ -6777,6 +7354,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", @@ -6790,6 +7382,42 @@ "node": ">=4" } }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=4.0" + } + }, "node_modules/estree-walker": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", @@ -6813,6 +7441,17 @@ "integrity": "sha512-4IJSItgS/41IxN5UVAVuAyczwZF7ZIEsM1XAoUzIHA6A+xzusEZUutdXz2Nr+MQPLxfTiCvqE79/C8HT8fKFvA==", "license": "MIT" }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/exenv": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz", @@ -6857,6 +7496,27 @@ "node": ">=0.10.0" } }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -6888,6 +7548,16 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "peer": true, + "dependencies": { + "pend": "~1.2.0" + } + }, "node_modules/fdir": { "version": "6.4.6", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", @@ -7082,6 +7752,21 @@ "devOptional": true, "license": "MIT" }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "license": "MIT", + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, "node_modules/fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -7203,7 +7888,6 @@ "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -7263,6 +7947,22 @@ "node": ">= 0.4" } }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "peer": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-symbol-description": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", @@ -7310,6 +8010,47 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true + }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "license": "BSD-3-Clause", + "optional": true, + "peer": true, + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/global-agent/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -7323,7 +8064,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "define-properties": "^1.2.1", @@ -7357,11 +8098,36 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, "node_modules/gray-matter": { @@ -7420,7 +8186,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "es-define-property": "^1.0.0" @@ -7696,6 +8462,27 @@ "entities": "^4.4.0" } }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause", + "peer": true + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "license": "MIT", + "peer": true, + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -8314,6 +9101,39 @@ "node": ">=10" } }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/js-sha256": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.10.1.tgz", @@ -8351,6 +9171,13 @@ "node": ">=6" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "license": "MIT", + "peer": true + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -8371,11 +9198,18 @@ "dev": true, "license": "MIT" }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "license": "ISC", + "optional": true, + "peer": true + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, "license": "MIT", "bin": { "json5": "lib/cli.js" @@ -8384,6 +9218,16 @@ "node": ">=6" } }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "license": "MIT", + "peer": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/jsonpointer": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", @@ -8486,6 +9330,16 @@ "prebuild-install": "^7.0.1" } }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "license": "MIT", + "peer": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -8520,6 +9374,17 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "license": "MIT" }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.11.5" + } + }, "node_modules/loader-utils": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", @@ -8684,6 +9549,16 @@ "tslib": "^2.0.3" } }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -8754,6 +9629,20 @@ "node": ">= 18" } }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -9051,6 +9940,14 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/micromark": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.2.0.tgz", @@ -9654,6 +10551,16 @@ "node": ">= 0.6" } }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -9804,6 +10711,14 @@ "devOptional": true, "license": "MIT" }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/no-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", @@ -9901,6 +10816,19 @@ "node": ">=0.10.0" } }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/npmlog": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", @@ -9953,7 +10881,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -9984,7 +10912,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "devOptional": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -10038,6 +10965,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -10141,6 +11078,13 @@ "canvas": "^2.11.2" } }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT", + "peer": true + }, "node_modules/phone": { "version": "3.1.62", "resolved": "https://registry.npmjs.org/phone/-/phone-3.1.62.tgz", @@ -10331,6 +11275,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -10362,7 +11316,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", - "devOptional": true, "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", @@ -10391,6 +11344,19 @@ "integrity": "sha512-sqo7otiDq5rA4djRkFI7IjLQqxRrLpIou0d3rqr03JJLUGf5raPh91xCio+lFFbQf0SlcVckStz0EmDEX3EeZA==", "license": "MIT" }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -10904,6 +11870,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "license": "MIT", + "peer": true + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -10913,6 +11886,19 @@ "node": ">=4" } }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "license": "MIT", + "peer": true, + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -10930,6 +11916,33 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "license": "BSD-3-Clause", + "optional": true, + "peer": true, + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/roarr/node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause", + "optional": true, + "peer": true + }, "node_modules/rollup": { "version": "4.43.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.43.0.tgz", @@ -11148,6 +12161,31 @@ "semver": "bin/semver.js" } }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/serialize-javascript": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", @@ -11695,6 +12733,19 @@ "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", "license": "MIT" }, + "node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "debug": "^4.1.0" + }, + "engines": { + "node": ">= 8.0" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -11727,6 +12778,17 @@ "dev": true, "license": "MIT" }, + "node_modules/tapable": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", + "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, "node_modules/tar": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", @@ -11850,6 +12912,103 @@ "node": ">=10" } }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", + "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", + "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/terser/node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -11979,6 +13138,20 @@ "node": "*" } }, + "node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "license": "(MIT OR CC0-1.0)", + "optional": true, + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -12080,7 +13253,6 @@ "version": "7.8.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", - "dev": true, "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { @@ -12270,6 +13442,16 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/unraw": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/unraw/-/unraw-3.0.0.tgz", @@ -12667,6 +13849,21 @@ "loose-envify": "^1.0.0" } }, + "node_modules/watchpack": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", + "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/web-namespaces": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", @@ -12696,6 +13893,127 @@ "node": ">=12" } }, + "node_modules/webpack": { + "version": "5.99.9", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.9.tgz", + "integrity": "sha512-brOPwM3JnmOa+7kd3NsmOUOwbDAj8FT9xDsG3IW0MgbN9yZV7Oi/s/+MNQ/EcSMqw7qfoRyXPoeEWT8zLVdVGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.14.0", + "browserslist": "^4.24.0", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.1", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.2", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.11", + "watchpack": "^2.4.1", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-sources": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/webpack/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", + "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/whatwg-mimetype": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", @@ -13364,7 +14682,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "devOptional": true, "license": "ISC" }, "node_modules/y18n": { @@ -13421,6 +14738,17 @@ "node": ">=12" } }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "peer": true, + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, "node_modules/zod": { "version": "3.25.67", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.67.tgz", diff --git a/apps/web/package.json b/apps/web/package.json index 94e50610d..3d5bf9535 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -32,6 +32,7 @@ "@notesnook/themes-server": "file:../../servers/themes", "@notesnook/ui": "file:../../packages/ui", "@notesnook/web-clipper": "file:../../extensions/web-clipper", + "@paddle/paddle-js": "^1.4.2", "@react-pdf-viewer/core": "^3.12.0", "@react-pdf-viewer/toolbar": "^3.12.0", "@rehookify/datepicker": "^6.6.7", diff --git a/apps/web/src/bootstrap.tsx b/apps/web/src/bootstrap.tsx index 5cc7ddfec..453970eb6 100644 --- a/apps/web/src/bootstrap.tsx +++ b/apps/web/src/bootstrap.tsx @@ -53,7 +53,10 @@ export type Routes = keyof typeof routes; const routes = { "/checkout": { - component: () => import("./views/checkout"), + component: () => import("./views/checkout") + }, + "/payments": { + component: () => import("./views/payments"), props: {} }, "/account/recovery": { @@ -96,6 +99,7 @@ const routes = { } as const; const sessionExpiryExceptions: Routes[] = [ + "/payments", "/recover", "/account/recovery", "/sessionexpired", diff --git a/apps/web/src/common/db.ts b/apps/web/src/common/db.ts index 886eb0c94..f9774ef25 100644 --- a/apps/web/src/common/db.ts +++ b/apps/web/src/common/db.ts @@ -39,14 +39,23 @@ async function initializeDatabase(persistence: DatabasePersistence) { await useKeyStore.getState().setValue("databaseKey", databaseKey); } + // db.host({ + // API_HOST: "https://api.notesnook.com", + // AUTH_HOST: "https://auth.streetwriters.co", + // SSE_HOST: "https://events.streetwriters.co", + // ISSUES_HOST: "https://issues.streetwriters.co", + // MONOGRAPH_HOST: "https://monogr.ph", + // SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co", + // ...Config.get("serverUrls", {}) + // }); + const base = `http://localhost`; db.host({ - API_HOST: "https://api.notesnook.com", - AUTH_HOST: "https://auth.streetwriters.co", - SSE_HOST: "https://events.streetwriters.co", - ISSUES_HOST: "https://issues.streetwriters.co", - MONOGRAPH_HOST: "https://monogr.ph", - SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co", - ...Config.get("serverUrls", {}) + API_HOST: `${base}:5264`, + AUTH_HOST: `${base}:8264`, + SSE_HOST: `${base}:7264`, + ISSUES_HOST: `${base}:2624`, + SUBSCRIPTIONS_HOST: `${base}:9264`, + MONOGRAPH_HOST: `${base}:6264` }); const storage = new NNStorage( diff --git a/apps/web/src/components/navigation-menu/index.tsx b/apps/web/src/components/navigation-menu/index.tsx index f38e1131c..59dfbef8e 100644 --- a/apps/web/src/components/navigation-menu/index.tsx +++ b/apps/web/src/components/navigation-menu/index.tsx @@ -106,6 +106,7 @@ import { CREATE_BUTTON_MAP } from "../../common"; import { useStore as useNotebookStore } from "../../stores/notebook-store"; import { useStore as useTagStore } from "../../stores/tag-store"; import { showSortMenu } from "../group-header"; +import { BuyDialog } from "../../dialogs/buy-dialog"; type Route = { id: "notes" | "favorites" | "reminders" | "monographs" | "trash" | "archive"; @@ -792,6 +793,7 @@ function NavigationDropdown() { title: strings.upgradeToPro(), icon: Pro.path, key: "upgrade", + onClick: () => BuyDialog.show({}), isHidden: notLoggedIn || isPro }, { diff --git a/apps/web/src/dialogs/buy-dialog/buy-dialog.tsx b/apps/web/src/dialogs/buy-dialog/buy-dialog.tsx index 259efd5c5..369815f2c 100644 --- a/apps/web/src/dialogs/buy-dialog/buy-dialog.tsx +++ b/apps/web/src/dialogs/buy-dialog/buy-dialog.tsx @@ -29,9 +29,13 @@ import Field from "../../components/field"; import { hardNavigate } from "../../navigation"; import { Features } from "./features"; import { PaddleCheckout } from "./paddle"; -import { Period, Plan, PricingInfo } from "./types"; -import { PLAN_METADATA, usePlans } from "./plans"; -import { formatPeriod, getFullPeriod, PlansList } from "./plan-list"; +import { Period, Plan, PlanId, Price, PricingInfo } from "./types"; +import { usePlans } from "./plans"; +import { + formatRecurringPeriodShort, + getFullPeriod, + PlansList +} from "./plan-list"; import { showToast } from "../../utils/toast"; import { TaskManager } from "../../common/task-manager"; import { db } from "../../common/db"; @@ -48,7 +52,8 @@ import { strings } from "@notesnook/intl"; type BuyDialogProps = BaseDialogProps & { couponCode?: string; - plan?: "monthly" | "yearly" | "education"; + plan?: PlanId; + onClose: () => void; }; export const BuyDialog = DialogManager.register(function BuyDialog( @@ -110,7 +115,7 @@ export const BuyDialog = DialogManager.register(function BuyDialog( > onClose(false)} - initialPlan={plan} + initialPlan={plan || "free"} user={user} /> @@ -121,7 +126,7 @@ export const BuyDialog = DialogManager.register(function BuyDialog( }); type SideBarProps = { - initialPlan?: Period; + initialPlan: PlanId; onClose: () => void; user?: User; }; @@ -130,6 +135,7 @@ export function CheckoutSideBar(props: SideBarProps) { const [showPlans, setShowPlans] = useState(false); const onPlanSelected = useCheckoutStore((state) => state.selectPlan); const selectedPlan = useCheckoutStore((state) => state.selectedPlan); + const selectedPrice = useCheckoutStore((state) => state.selectedPrice); const pricingInfo = useCheckoutStore((state) => state.pricingInfo); const couponCode = useCheckoutStore((store) => store.couponCode); const onApplyCoupon = useCheckoutStore((store) => store.applyCoupon); @@ -137,10 +143,11 @@ export function CheckoutSideBar(props: SideBarProps) { if (isCheckoutCompleted) return ; - if (user && selectedPlan) + if (user && selectedPlan && selectedPrice) return ( { onApplyCoupon(undefined); @@ -161,13 +168,14 @@ export function CheckoutSideBar(props: SideBarProps) { ); } - if (user && (showPlans || !!initialPlan)) + if (user) return ( { - if (!initialPlan || showPlans) return; - const plan = plans.find((p) => p.period === initialPlan); - onPlanSelected(plan); + // if (!initialPlan || showPlans) return; + // const plan = plans.find((p) => p.id === initialPlan); + // onPlanSelected(plan); }} onPlanSelected={onPlanSelected} /> @@ -207,25 +215,23 @@ export function CheckoutDetails({ user?: { id: string; email: string }; }) { const selectedPlan = useCheckoutStore((state) => state.selectedPlan); + const selectedPrice = useCheckoutStore((state) => state.selectedPrice); const onPriceUpdated = useCheckoutStore((state) => state.updatePrice); const completeCheckout = useCheckoutStore((state) => state.completeCheckout); const isCheckoutCompleted = useCheckoutStore((store) => store.isCompleted); const couponCode = useCheckoutStore((store) => store.couponCode); - const setIsApplyingCoupon = useCheckoutStore( - (store) => store.setIsApplyingCoupon - ); const theme = useThemeStore((store) => store.colorScheme); if (isCheckoutCompleted) return null; - if (selectedPlan && user) + if (selectedPlan && user && selectedPrice) return ( setIsApplyingCoupon(true)} onPriceUpdated={(pricingInfo) => { onPriceUpdated(pricingInfo); // console.log( @@ -269,9 +275,9 @@ function TrialOrUpgrade(props: TrialOrUpgradeProps) { ) : ( - Starting from {getCurrencySymbol(plan.currency)} + {/* Starting from {getCurrencySymbol(plan.currency)} {plan.price.gross} - {formatPeriod(plan.period)} + {formatPeriod(plan.period)} */} )} {isMacStoreApp() ? ( @@ -404,12 +410,12 @@ export function CheckoutCompleted(props: { type SelectedPlanProps = { plan: Plan; + price: Price; pricingInfo: PricingInfo | undefined; onChangePlan?: () => void; }; function SelectedPlan(props: SelectedPlanProps) { - const { plan, pricingInfo, onChangePlan } = props; - const metadata = PLAN_METADATA[plan.period]; + const { plan, price, pricingInfo, onChangePlan } = props; const [isApplyingCoupon, setIsApplyingCoupon] = useCheckoutStore((store) => [ store.isApplyingCoupon, store.setIsApplyingCoupon @@ -448,15 +454,15 @@ function SelectedPlan(props: SelectedPlanProps) { return ( <> - {plan.period === "monthly" ? ( + {price.period === "monthly" ? ( ) : ( )} @@ -468,9 +474,9 @@ function SelectedPlan(props: SelectedPlanProps) { mt={1} sx={{ fontSize: "subheading", textAlign: "center" }} > - {metadata.title} + {plan.title} - {plan.period === "education" && ( + {plan.id === "education" && ( Change plan @@ -549,43 +556,30 @@ type CheckoutPricingProps = { }; export function CheckoutPricing(props: CheckoutPricingProps) { const { pricingInfo } = props; - const { currency, price, discount, period, recurringPrice } = pricingInfo; + const { price, discount, period, recurringPrice } = pricingInfo; const fields = [ { key: "subtotal", label: "Subtotal", - value: formatPrice(currency, price.net.toFixed(2), null) + value: price.subtotal }, { key: "tax", label: "Sales tax", color: "red", - value: formatPrice(currency, price.tax.toFixed(2), null) + value: price.tax }, { key: "discount", label: "Discount", - color: "accent", - value: formatPrice( - currency, - discount.amount.toFixed(2), - null, - discount.amount > 0 - ) + color: "green", + value: price.discount } ]; - const isDiscounted = discount.recurring || discount.amount <= 0; - const currentTotal = formatPrice( - currency, - (price.gross - discount.amount).toFixed(2), - isDiscounted ? period : undefined - ); - const recurringTotal = formatPrice( - currency, - recurringPrice.gross.toFixed(2), - period - ); + const isRecurringDiscount = !discount || discount.recurring; + const currentTotal = price.total; + const recurringTotal = recurringPrice ? recurringPrice.total : undefined; return ( <> {fields.map((field) => ( @@ -627,19 +621,12 @@ export function CheckoutPricing(props: CheckoutPricingProps) { > {currentTotal} - - {period === "education" && discount.amount > 0 - ? "for one year" - : isDiscounted - ? "forever" - : `first ${getFullPeriod(period)} then ${recurringTotal}`} + + {recurringTotal + ? isRecurringDiscount + ? "forever" + : `first ${getFullPeriod(period)} then ${recurringTotal}` + : "for one year"} @@ -653,7 +640,7 @@ function formatPrice( period?: Period | null, negative = false ) { - const formattedPeriod = period ? formatPeriod(period) : ""; + const formattedPeriod = period ? formatRecurringPeriodShort(period) : ""; const currencySymbol = getCurrencySymbol(currency); const prefix = negative ? "-" : ""; return `${prefix}${currencySymbol}${price}${formattedPeriod}`; diff --git a/apps/web/src/dialogs/buy-dialog/helpers.ts b/apps/web/src/dialogs/buy-dialog/helpers.ts index 227e6bee6..813c1ce05 100644 --- a/apps/web/src/dialogs/buy-dialog/helpers.ts +++ b/apps/web/src/dialogs/buy-dialog/helpers.ts @@ -22,6 +22,17 @@ import { ICurrencySymbols } from "@brixtol/currency-symbols"; +export const IS_DEV = import.meta.env.DEV || IS_TESTING; export function getCurrencySymbol(currency: string) { return _getSymbol(currency as keyof ICurrencySymbols) || currency; } + +export function parseAmount(amount: string) { + const matches = /(.+?)([\d.]+)/.exec(amount); + if (!matches || matches.length < 3) return; + return { + formatted: amount, + symbol: matches[1], + amount: parseFloat(matches[2]) + }; +} diff --git a/apps/web/src/dialogs/buy-dialog/paddle.tsx b/apps/web/src/dialogs/buy-dialog/paddle.tsx index ec5804a21..07096e44d 100644 --- a/apps/web/src/dialogs/buy-dialog/paddle.tsx +++ b/apps/web/src/dialogs/buy-dialog/paddle.tsx @@ -20,187 +20,141 @@ along with this program. If not, see . import { useEffect, useState, useRef, useCallback } from "react"; import { Flex } from "@theme-ui/components"; import { Loader } from "../../components/loader"; -import { - CheckoutData, - CheckoutDataResponse, - CheckoutPrices, - PaddleEvent, - PaddleEvents, - Plan, - Price, - PricingInfo -} from "./types"; +import { PaddleEvent, Period, Plan, Price, PricingInfo } from "./types"; import { ScrollContainer } from "@notesnook/ui"; import useMobile from "../../hooks/use-mobile"; -import { logger } from "../../utils/logger"; +import { + AvailablePaymentMethod, + CheckoutEventNames, + CheckoutOpenLineItem, + CurrencyCode, + Totals, + Product, + CheckoutEventsCustomerAddress, + CheckoutEventsData +} from "@paddle/paddle-js"; import { isFeatureSupported } from "../../utils/feature-check"; -import { isDev } from "./plans"; +import { IS_DEV, parseAmount } from "./helpers"; +import { CheckoutCustomerUserInfo } from "@paddle/paddle-js/types/checkout/customer"; +import { logger } from "../../utils/logger"; -const VENDOR_ID = isDev ? 1506 : 128190; -const PADDLE_ORIGIN = isDev +export const SELLER_ID = IS_DEV ? 1506 : 128190; +export const CLIENT_PADDLE_TOKEN = IS_DEV + ? "test_e29ab18724934c1d35a05a7d2cb" + : "live_251f65dc0ac5ac364e44817fe92"; +const PADDLE_ORIGIN = IS_DEV ? "https://sandbox-buy.paddle.com" : "https://buy.paddle.com"; -const SUBSCRIPTION_MANAGEMENT_URL = isDev - ? "https://sandbox-subscription-management.paddle.com" - : "https://subscription-management.paddle.com"; -const CHECKOUT_SERVICE_ORIGIN = isDev +const CHECKOUT_SERVICE = IS_DEV ? "https://sandbox-checkout-service.paddle.com" : "https://checkout-service.paddle.com"; +const PADDLE_API = IS_DEV + ? "https://sandbox-api.paddle.com" + : "https://api.paddle.com"; -const SUBSCRIBED_EVENTS: PaddleEvents[] = [ - PaddleEvents["Checkout.Loaded"], - PaddleEvents["Checkout.Coupon.Applied"], - PaddleEvents["Checkout.Coupon.Remove"], - PaddleEvents["Checkout.Location.Submit"], - PaddleEvents["Checkout.Complete"], - PaddleEvents["Checkout.Customer.Details"] +const SUBSCRIBED_EVENTS: CheckoutEventNames[] = [ + CheckoutEventNames.CHECKOUT_LOADED, + CheckoutEventNames.CHECKOUT_COMPLETED, + CheckoutEventNames.CHECKOUT_CUSTOMER_UPDATED ]; type PaddleCheckoutProps = { user: { id: string; email: string }; theme: "dark" | "light"; plan: Plan; + price: Price; onPriceUpdated?: (pricingInfo: PricingInfo) => void; - onCouponApplied?: () => void; onCompleted?: () => void; coupon?: string; }; export function PaddleCheckout(props: PaddleCheckoutProps) { - const { - plan, - onPriceUpdated, - coupon, - theme, - onCouponApplied, - onCompleted, - user - } = props; + const { plan, price, onPriceUpdated, coupon, onCompleted, user, theme } = + props; const [isLoading, setIsLoading] = useState(true); const appliedCouponCode = useRef(); - const checkoutId = useRef(); + const checkoutDataRef = useRef(); const checkoutRef = useRef(null); + const addressRef = useRef(); const isMobile = useMobile(); const reloadCheckout = useCallback(() => { - if (!checkoutRef.current) return; + if (!checkoutRef.current || !checkoutDataRef.current) return; setIsLoading(true); - checkoutRef.current.src = `${PADDLE_ORIGIN}/checkout/?checkout_id=${ - checkoutId.current - }&display_mode=inline&apple_pay_enabled=${isFeatureSupported( - "applePaySupported" - )}`; - }, []); - - const updatePrice = useCallback( - async (checkoutId: string, isInvalidCoupon?: boolean) => { - const checkoutData = await getCheckoutData(checkoutId); - if (!checkoutData) return; - const pricingInfo = getPricingInfo(plan, checkoutData); - pricingInfo.invalidCoupon = isInvalidCoupon; - if (onPriceUpdated) onPriceUpdated(pricingInfo); - return pricingInfo; - }, - [onPriceUpdated, plan] - ); - - const updateCoupon = useCallback( - async (checkoutId: string) => { - if ( - appliedCouponCode.current === coupon || - !appliedCouponCode.current === !coupon - ) - return false; - if (onCouponApplied) onCouponApplied(); - const checkoutData = coupon - ? await applyCoupon(checkoutId, coupon).catch(() => false) - : await removeCoupon(checkoutId).catch(() => false); - if (!checkoutData) { - await updatePrice(checkoutId, true).catch(() => false); - return false; - } - appliedCouponCode.current = coupon; - return true; - }, - [coupon, updatePrice, onCouponApplied] - ); + checkoutRef.current.src = "about:blank"; + checkoutRef.current.src = getCheckoutURL(checkoutDataRef.current.id, theme); + }, [theme]); useEffect(() => { - createCheckout({ plan, theme, user, coupon }).then((checkoutData) => { - if (!checkoutData) return; - const pricingInfo = getPricingInfo(plan, checkoutData); - if (onPriceUpdated) onPriceUpdated(pricingInfo); - appliedCouponCode.current = pricingInfo.coupon; - checkoutId.current = checkoutData.public_checkout_id; - reloadCheckout(); - }); - }, [plan, theme, user]); + if (checkoutDataRef.current) { + if ( + coupon && + (!checkoutDataRef.current.discount || + checkoutDataRef.current.discount.code !== coupon) + ) { + applyCoupon(checkoutDataRef.current.id, coupon).then(() => + reloadCheckout() + ); + } else if (!coupon && checkoutDataRef.current.discount) + removeDiscount( + checkoutDataRef.current.id, + checkoutDataRef.current.discount.id + ).then(() => reloadCheckout()); + } else { + createCheckout({ theme, user, coupon, price }).then( + async (checkoutData) => { + if (!checkoutData) return; + const pricingInfo = await getPrice(price, checkoutData); + if (!pricingInfo) return; + + addressRef.current = checkoutData.customer.address || undefined; + onPriceUpdated?.(pricingInfo); + appliedCouponCode.current = pricingInfo.coupon; + checkoutDataRef.current = checkoutData; + reloadCheckout(); + } + ); + } + }, [coupon, onPriceUpdated, price, reloadCheckout, theme, user]); useEffect(() => { async function onMessage(ev: MessageEvent) { if (ev.origin !== PADDLE_ORIGIN) return; logger.debug("Paddle event received", { data: ev.data }); - const { event, event_name, callback_data } = ev.data; - const { checkout } = callback_data || {}; + const { event_name, callback_data } = ev.data; - if (event === PaddleEvents["Checkout.RemoveSpinner"]) setIsLoading(false); - - if ( - !checkout || - !checkout.id || - (SUBSCRIBED_EVENTS.indexOf(event_name) === -1 && - SUBSCRIBED_EVENTS.indexOf(event) === -1) - ) { - logger.debug("Ignoring paddle event", { event_name, event }); + if (event_name === CheckoutEventNames.CHECKOUT_FAILED) { + setIsLoading(false); return; } - if (event_name === PaddleEvents["Checkout.Complete"]) { + if (!callback_data.data || SUBSCRIBED_EVENTS.indexOf(event_name) === -1) { + return; + } + + if (event_name === CheckoutEventNames.CHECKOUT_COMPLETED) { onCompleted && onCompleted(); return; } - if (event_name === PaddleEvents["Checkout.Loaded"]) { + if (event_name === CheckoutEventNames.CHECKOUT_LOADED) { setIsLoading(false); } - const pricingInfo = getPricingInfo(plan, { - public_checkout_id: checkout.id, - ip_geo_country_code: callback_data?.user?.country || "US", - items: [ - { - prices: checkout.prices.customer.items, - recurring: { - prices: [checkout.recurring_prices.customer.items[0].recurring] - } - } - ] - }); + addressRef.current = callback_data.data.customer.address || undefined; + const pricingInfo = await getPrice(price, callback_data.data); + if (!pricingInfo) return; + + pricingInfo.invalidCoupon = + !!props.coupon && !callback_data.data.discount; if (onPriceUpdated) onPriceUpdated(pricingInfo); appliedCouponCode.current = pricingInfo.coupon; - checkoutId.current = checkout.id; - - await updateCoupon(checkout.id); + checkoutDataRef.current = callback_data.data || checkoutDataRef.current; } window.addEventListener("message", onMessage, false); return () => { window.removeEventListener("message", onMessage, false); }; - }, [ - onPriceUpdated, - updatePrice, - plan, - onCompleted, - user.email, - reloadCheckout, - updateCoupon - ]); - - useEffect(() => { - if (!checkoutId.current) return; - updateCoupon(checkoutId.current).then((result) => { - if (result) reloadCheckout(); - }); - }, [coupon]); + }, [onPriceUpdated, plan, price, props.coupon, onCompleted]); return ( 0 ? price.discounts[0] : undefined; - const isRecurringDiscount = recurringPrice.discounts.length > 0; - - return { - country: checkoutData.ip_geo_country_code, - currency: price.currency, - discount: { - amount: discount?.gross_discount || 0, - recurring: isRecurringDiscount, - code: discount?.code, - type: "promo" - }, - period: plan.period, - price: normalizeCheckoutPrice(price), - recurringPrice: normalizeCheckoutPrice(recurringPrice), - coupon: discount?.code - }; -} - -function normalizeCheckoutPrice(prices: CheckoutPrices): Price { - const price = prices.unit_price; - return { - gross: price.gross, - net: price.net, - tax: price.tax, - currency: prices.currency - }; -} - -async function applyCoupon( - checkoutId: string, - couponCode: string -): Promise { - const url = ` ${CHECKOUT_SERVICE_ORIGIN}/checkout/${checkoutId}/coupon`; - const body = { data: { coupon_code: couponCode } }; - const headers = new Headers(); - headers.set("content-type", "application/json"); - const response = await fetch(url, { - body: JSON.stringify(body), - headers, - method: "POST" - }); - - if (!response.ok) return false; - const json = (await response.json()) as CheckoutDataResponse; - return json.data; -} - -async function submitCustomerInfo( - checkoutId: string, - email: string, - country: string -): Promise { - if (IS_TESTING) return false; - - const url = ` ${CHECKOUT_SERVICE_ORIGIN}/checkout/${checkoutId}/customer-info`; - const body = { - data: { - email, - country_code: country, - audience_optin: false, - postcode: "1123212" +async function getPrice(price: Price, checkoutData: CheckoutEventsData) { + const response = await fetch(`${PADDLE_API}/pricing-preview`, { + method: "POST", + body: JSON.stringify({ + items: [ + { + quantity: 1, + price_id: price.id + } + ], + currency_code: checkoutData.currency_code, + address: checkoutData.customer.address + ? { + postal_code: checkoutData.customer.address?.postal_code, + country_code: checkoutData.customer.address?.country_code + } + : undefined, + discount_id: checkoutData.discount?.id + }), + headers: { + "Content-Type": "application/json", + Accept: "application/json", + "Paddle-Clienttoken": CLIENT_PADDLE_TOKEN } + }); + if (!response.ok) return false; + const json = (await response.json()) as PricePreviewResponse; + console.log(json); + return getPricingInfo(json.data, price.period, checkoutData); +} + +function getPricingInfo( + price: PricePreviewResponse["data"], + period: Period, + checkoutData: CheckoutEventsData +): PricingInfo { + const { + discounts, + formatted_totals: totals, + totals: _totals, + price: _price, + tax_rate + } = price.details.line_items[0]; + const discount = discounts[0]; + const isRecurring = !discount || discount.discount.recur; + const totalsWithoutDiscount: Totals = { ...totals }; + if (discount) { + const taxRate = parseFloat(tax_rate); + const tax = parseInt(_totals.subtotal) * taxRate; + const total = parseInt(_totals.subtotal) + tax; + const { symbol = price.currency_code } = parseAmount(totals.subtotal) || {}; + totalsWithoutDiscount.discount = `${symbol}0.00`; + totalsWithoutDiscount.subtotal = totals.subtotal; + totalsWithoutDiscount.tax = `${symbol}${(tax / 100).toFixed(2)}`; + totalsWithoutDiscount.total = `${symbol}${(total / 100).toFixed(2)}`; + } + return { + country: checkoutData.customer.address?.country_code || "US", + discount: discount + ? { + recurring: isRecurring, + amount: 0, + type: "promo" + } + : undefined, + period, + price: { + id: _price.id, + period, + currency: checkoutData.currency_code, + ...totals + }, + recurringPrice: { + id: _price.id, + period, + currency: checkoutData.currency_code, + ...totalsWithoutDiscount + }, + coupon: checkoutData.discount?.code }; - const headers = new Headers(); - headers.set("content-type", "application/json"); - const response = await fetch(url, { - body: JSON.stringify(body), - headers, - method: "POST" - }); - - if (!response.ok) return false; - - const json = (await response.json()) as CheckoutDataResponse; - return json.data; } -async function removeCoupon(checkoutId: string): Promise { - const url = ` ${CHECKOUT_SERVICE_ORIGIN}/checkout/${checkoutId}/coupon`; - - const response = await fetch(url, { - method: "DELETE" - }); - if (!response.ok) return false; - const json = (await response.json()) as CheckoutDataResponse; - return json.data; +interface Discount { + id: string; + status: "active" | "archived" | "expired" | "used"; + description: string; + enabled_for_checkout: boolean; + code: string | null; + type: "flat" | "flat_per_seat" | "percentage"; + amount: string; + currency_code: CurrencyCode | null; + recur: boolean; + maximum_recurring_intervals: number | null; + usage_limit: number | null; + restrict_to: string[] | null; + expires_at: string | null; + times_used: number; + created_at: string; + updated_at: string; } -async function getCheckoutData( - checkoutId: string -): Promise { - const url = `${CHECKOUT_SERVICE_ORIGIN}/checkout/${checkoutId}`; - const response = await fetch(url); - if (!response.ok) return undefined; - const json = (await response.json()) as CheckoutDataResponse; - return json.data; +interface DiscountLineItem { + discount: Discount; + total: string; + formatted_total: string; +} + +interface LineItem { + price: Price; + quantity: number; + tax_rate: string; + unit_totals: Totals; + formatted_unit_totals: Totals; + totals: Totals; + formatted_totals: Totals; + product: Product; + discounts: DiscountLineItem[]; +} +interface PricePreviewResponse { + data: { + customer_id: string | null; + address_id: string | null; + business_id: string | null; + currency_code: CurrencyCode; + address: { + country_code: string; + postal_code: string | null; + } | null; + customer_ip_address: string | null; + discount_id: string | null; + details: { + line_items: LineItem[]; + }; + availablePaymentMethods: AvailablePaymentMethod[]; + }; + // meta: { + // requestId: string; + // }; +} + +enum THEME { + LIGHT = "light", + DARK = "dark", + GREEN = "green" +} +enum DISPLAY_MODE { + OVERLAY = "overlay", + INLINE = "inline", + WIDE_OVERLAY = "wide-overlay" +} +interface CheckoutOutputAttributesProps { + customer?: CheckoutCustomerUserInfo & { + address?: Partial; + }; + custom_data?: string; + items?: InternalCheckoutOpenLineItem[]; + customer_auth_token?: string; + discount_code?: string; + discount_id?: string; + transaction_id?: string; + settings?: { + locale?: string; + theme?: THEME; + success_url?: string; + allow_logout?: boolean; + show_add_discounts?: boolean; + allow_discount_removal?: boolean; + show_add_tax_id?: boolean; + frame_target?: string; + frame_initial_height?: number; + frame_style?: string; + display_mode?: DISPLAY_MODE; + source_page?: string; + allowed_payment_methods?: AvailablePaymentMethod[]; + }; + seller_id?: number | null; + client_token?: string; + apple_pay_enabled?: boolean; + checkout_initiated?: number; + "paddlejs-version"?: string | null; +} + +type InternalCheckoutOpenLineItem = Omit & { + price_id?: string; +}; + +function getCheckoutSettings( + theme: PaddleCheckoutProps["theme"] +): CheckoutOutputAttributesProps["settings"] { + return { + allow_discount_removal: true, + allowed_payment_methods: [ + "apple_pay", + "card", + "google_pay", + "paypal", + "alipay", + "bancontact", + "ideal" + ], + display_mode: DISPLAY_MODE.INLINE, + allow_logout: false, + show_add_discounts: true, + theme: theme === "dark" ? THEME.DARK : THEME.LIGHT, + source_page: window.location.href + }; +} + +interface CheckoutDataResponse { + data: CheckoutEventsData & { ip_geo_country_code: string }; } async function createCheckout(props: { - plan: PaddleCheckoutProps["plan"]; + price: PaddleCheckoutProps["price"]; user: PaddleCheckoutProps["user"]; theme: PaddleCheckoutProps["theme"]; coupon?: PaddleCheckoutProps["coupon"]; }) { - const { plan, user, theme, coupon } = props; - const url = getCheckoutURL({ plan, user, theme }); - const response = await fetch(url); + const { user, theme, coupon, price } = props; + const response = await fetch(`${CHECKOUT_SERVICE}/transaction-checkout`, { + method: "POST", + body: JSON.stringify({ + data: { + custom_data: JSON.stringify({ userId: user.id }), + customer: { email: user.email }, + items: [{ price_id: price.id, quantity: 1 }], + settings: getCheckoutSettings(theme) + } + }), + headers: { + "Content-Type": "application/json", + "paddle-clienttoken": CLIENT_PADDLE_TOKEN + } + }); if (!response.ok) return false; const json = (await response.json()) as CheckoutDataResponse; let checkoutData = json.data; - const checkoutId = checkoutData.public_checkout_id; + const checkoutId = checkoutData.id; checkoutData = await submitCustomerInfo( checkoutId, @@ -413,3 +462,66 @@ async function createCheckout(props: { return checkoutData; } + +async function submitCustomerInfo( + checkoutId: string, + email: string, + country: string +): Promise { + if (IS_TESTING) return false; + + const url = `${CHECKOUT_SERVICE}/transaction-checkout/${checkoutId}/customer`; + const body = { + data: { + customer: { + email, + marketing_consent: false, + address: { country_code: country, postal_code: "1123212" } + } + } + }; + const headers = new Headers(); + headers.set("content-type", "application/json"); + const response = await fetch(url, { + body: JSON.stringify(body), + headers, + method: "POST" + }); + + if (!response.ok) return false; + + const json = (await response.json()) as CheckoutDataResponse; + return json.data; +} + +async function applyCoupon( + checkoutId: string, + couponCode: string +): Promise { + const url = `${CHECKOUT_SERVICE}/transaction-checkout/${checkoutId}/discount`; + const body = { data: { discount_code: couponCode } }; + const headers = new Headers(); + headers.set("content-type", "application/json"); + const response = await fetch(url, { + body: JSON.stringify(body), + headers, + method: "PATCH" + }); + + if (!response.ok) return false; + const json = (await response.json()) as CheckoutDataResponse; + return json.data; +} + +async function removeDiscount( + checkoutId: string, + discountId: string +): Promise { + const url = `${CHECKOUT_SERVICE}/transaction-checkout/${checkoutId}/discount/${discountId}`; + const response = await fetch(url, { + method: "DELETE" + }); + if (!response.ok) return false; + const json = (await response.json()) as CheckoutDataResponse; + return json.data; +} diff --git a/apps/web/src/dialogs/buy-dialog/plan-list.tsx b/apps/web/src/dialogs/buy-dialog/plan-list.tsx index 8c96539cb..9568eeb5b 100644 --- a/apps/web/src/dialogs/buy-dialog/plan-list.tsx +++ b/apps/web/src/dialogs/buy-dialog/plan-list.tsx @@ -20,19 +20,36 @@ along with this program. If not, see . import { Text, Flex, Button, Image } from "@theme-ui/components"; import { Loading } from "../../components/icons"; import Nomad from "../../assets/nomad.svg?url"; -import { Period, Plan } from "./types"; -import { PLAN_METADATA, usePlans } from "./plans"; -import { useEffect } from "react"; -import { getCurrencySymbol } from "./helpers"; +import { Period, Plan, PlanId, Price } from "./types"; +import { usePlans } from "./plans"; +import { useEffect, useState } from "react"; +import { getCurrencySymbol, parseAmount } from "./helpers"; +import { strings } from "@notesnook/intl"; type PlansListProps = { - onPlanSelected: (plan: Plan) => void; + selectedPlan: PlanId; + onPlanSelected: (plan: Plan, price: Price) => void; onPlansLoaded?: (plans: Plan[]) => void; }; +const periods: { id: Period; title: string }[] = [ + { + title: strings.monthly(), + id: "monthly" + }, + { + title: strings.yearly(), + id: "yearly" + }, + { + id: "5-year", + title: "5 year" + } +]; export function PlansList(props: PlansListProps) { - const { onPlanSelected, onPlansLoaded } = props; + const { onPlanSelected, onPlansLoaded, selectedPlan } = props; const { isLoading, plans, discount, country } = usePlans(); - + const [selectedPeriod, setPeriod] = useState("yearly"); + console.log({ selectedPlan }); useEffect(() => { if (isLoading || !onPlansLoaded) return; onPlansLoaded(plans); @@ -40,7 +57,10 @@ export function PlansList(props: PlansListProps) { return ( <> - + Choose a plan @@ -54,32 +74,67 @@ export function PlansList(props: PlansListProps) { "Notesnook profits when you purchase a subscription — not by selling your data." )} + + {periods.map((period) => ( + + ))} + {plans.map((plan) => { - const metadata = PLAN_METADATA[plan.period]; + const price = plan.prices.find((p) => p.period === selectedPeriod); + if (!price) return null; + // const metadata = PLAN_METADATA[plan.period]; return ( ); })} @@ -108,17 +169,23 @@ export function PlansList(props: PlansListProps) { ); } -type RecurringPricingProps = { +type PricingProps = { plan: Plan; + price: Price; }; -function RecurringPricing(props: RecurringPricingProps) { - const { plan } = props; +function RecurringPricing(props: PricingProps) { + const { plan, price } = props; + // const price = plan.prices.find((p) => p.period === period); + // if (!price) return null; + const monthPrice = plan.prices.find( + (p) => p.period === "monthly" && price.period !== p.period + ); return ( - {plan.originalPrice && plan.originalPrice.gross !== plan.price.gross ? ( + {/* {plan.originalPrice && plan.originalPrice.gross !== plan.price.gross && ( - ) : null} - - - {getCurrencySymbol(plan.currency)} - {plan.price.gross} + )} */} + {/* {monthPrice && ( + + {getCurrencySymbol(price.currency)} + {monthPrice.gross} - {formatPeriod(plan.period)} + )} */} + + {monthPrice && monthPrice.subtotal < price.subtotal && ( + + {getCurrencySymbol(price.currency)} + {monthPrice.subtotal} + + )}{" "} + {getCurrencySymbol(price.currency)} + {price.subtotal} + /month + + {parseAmount(price.subtotal)?.amount === 0 ? null : ( + + billed {formatRecurringPeriod(price.period)} + + )} + + ); +} + +function OneTimePricing(props: PricingProps) { + const { price } = props; + return ( + + + {getCurrencySymbol(price.currency)} + {price.subtotal} + + + {formatOneTimePeriod(price.period)} ); } -export function formatPeriod(period: Period) { +export function formatOneTimePeriod(period: Period) { + return period === "monthly" + ? "for 1 month" + : period === "yearly" + ? "for 1 year" + : period === "5-year" + ? "for 5 years" + : ""; +} + +export function formatRecurringPeriod(period: Period) { + return period === "monthly" + ? "monthly" + : period === "yearly" + ? "annually" + : period === "5-year" + ? "every 5 years" + : ""; +} + +export function formatRecurringPeriodShort(period: Period) { return period === "monthly" ? "/mo" - : period === "yearly" || period === "education" + : period === "yearly" ? "/yr" + : period === "5-year" + ? "/5yr" : ""; } diff --git a/apps/web/src/dialogs/buy-dialog/plans.ts b/apps/web/src/dialogs/buy-dialog/plans.ts index 4be5c9df5..d4135eac8 100644 --- a/apps/web/src/dialogs/buy-dialog/plans.ts +++ b/apps/web/src/dialogs/buy-dialog/plans.ts @@ -18,58 +18,129 @@ along with this program. If not, see . */ import { useEffect, useState } from "react"; -import { Period, Plan } from "./types"; +import { Period, Plan, Price } from "./types"; +import { IS_DEV } from "./helpers"; -type PlanMetadata = { - title: string; - subtitle: string; -}; +function createPrice(id: string, period: Period, subtotal: number): Price { + return { + id, + period, + subtotal, + total: 0, + tax: 0, + currency: "USD" + }; +} -export const isDev = import.meta.env.DEV || IS_TESTING; - -export const EDUCATION_PLAN: Plan = { - id: isDev ? "50305" : "658759", - period: "education", - country: "US", - currency: "USD", - discount: { type: "regional", amount: 0, recurring: false }, - price: { gross: 9.99, net: 0, tax: 0 }, - originalPrice: { gross: 9.99, net: 0, tax: 0 } +const FREE_PLAN: Plan = { + id: "free", + title: "Free", + recurring: true, + prices: [ + createPrice("monthly", "monthly", 0), + createPrice("yearly", "yearly", 0), + createPrice("5-year", "5-year", 0) + ] }; export const DEFAULT_PLANS: Plan[] = [ + FREE_PLAN, { - period: "monthly", - country: "PK", - currency: "USD", - discount: { type: "regional", amount: 0, recurring: false }, - originalPrice: { gross: 4.49, net: 0, tax: 0 }, - id: isDev ? "9822" : "648884", - price: { gross: 4.49, net: 0, tax: 0 } + id: "essential", + title: "Essential", + recurring: true, + prices: [ + createPrice( + IS_DEV + ? "pri_01j00cf6v5kqqvchcpgapr7123" + : "pri_01j02dbe7btgk6ta3ctper2161", + "monthly", + 1.99 + ), + createPrice( + IS_DEV + ? "pri_01j00d1qq3bart3w1rvt0q8bkt" + : "pri_01j02dckdey85cgmrdknd2f4zx", + "yearly", + 1.24 + ) + ] }, { - period: "yearly", - country: "PK", - currency: "USD", - discount: { type: "regional", amount: 0, recurring: false }, - id: isDev ? "50305" : "658759", - price: { gross: 49.99, net: 0, tax: 0 }, - originalPrice: { gross: 49.99, net: 0, tax: 0 } + id: "pro", + title: "Pro", + recurring: true, + prices: [ + createPrice( + IS_DEV + ? "pri_01j00fnbzth05aafjb05kcahvq" + : "pri_01h9qprh1xvvxbs8vcpcg7qacm", + "monthly", + 6.49 + ), + createPrice( + IS_DEV + ? "pri_01j00fpawjwkrqxy2faqhzts9m" + : "pri_01h9qpqyjwbm3m2xy7834t3azt", + "yearly", + 5.49 + ), + createPrice( + IS_DEV + ? "pri_01j00fr72gn40xzk9cdcfpzevw" + : "pri_01j02da6n9c1xmzq15kjhjxngn", + "5-year", + 4.49 + ) + ] }, - EDUCATION_PLAN -]; - -export const PLAN_METADATA: Record = { - monthly: { title: "Monthly", subtitle: `Pay once a month.` }, - yearly: { title: "Yearly", subtitle: `Pay once a year.` }, - education: { + { + id: "believer", + title: "Believer", + recurring: true, + prices: [ + createPrice( + IS_DEV + ? "pri_01j00fxsryh5jfyfjqq5tsx4c7" + : "pri_01j02ddzyc1m63s3b1kq6g4bnn", + "monthly", + 7.49 + ), + createPrice( + IS_DEV + ? "pri_01j00fzbz01rfn3f30crwxc7y9" + : "pri_01j02dezv9v5ncw3e16ncvz7x7", + "yearly", + 6.49 + ), + createPrice( + IS_DEV + ? "pri_01j00g0wpmj6m9vcvpjq97jwpp" + : "pri_01j02dfxz6y8hghfbr5p8cqtgb", + "5-year", + 5.49 + ) + ] + }, + { + id: "education", title: "Education", - subtitle: "Special offer for students & teachers." + recurring: false, + prices: [ + createPrice( + IS_DEV + ? "pri_01j00g6asxjskghjcrbxpbd26e" + : "pri_01j02dh4mwkbsvpygyf1bd9whs", + "yearly", + 19.99 + ) + ] } -}; +]; let CACHED_PLANS: Plan[]; export async function getPlans(): Promise { + return DEFAULT_PLANS; if (IS_TESTING || import.meta.env.DEV) return DEFAULT_PLANS; if (CACHED_PLANS) return CACHED_PLANS; @@ -77,7 +148,7 @@ export async function getPlans(): Promise { const response = await fetch(url); if (!response.ok) return null; const plans = (await response.json()) as Plan[]; - plans.push(EDUCATION_PLAN); + // plans.push(EDUCATION_PLAN); CACHED_PLANS = plans; return plans; } @@ -94,8 +165,8 @@ export function usePlans() { const plans = await getPlans(); if (!plans) return; setPlans(plans); - setDiscount(Math.max(...plans.map((p) => p.discount?.amount || 0))); - setCountry(plans[0].country); + // setDiscount(Math.max(...plans.map((p) => p.discount?.amount || 0))); + // setCountry(plans[0].country); } catch (e) { console.error(e); } finally { diff --git a/apps/web/src/dialogs/buy-dialog/store.ts b/apps/web/src/dialogs/buy-dialog/store.ts index efb8d3ab8..8eaa39cb1 100644 --- a/apps/web/src/dialogs/buy-dialog/store.ts +++ b/apps/web/src/dialogs/buy-dialog/store.ts @@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -import { Plan, PricingInfo } from "./types"; +import { Plan, Price, PricingInfo } from "./types"; import { create } from "zustand"; import { create as produce } from "mutative"; @@ -25,7 +25,8 @@ interface ICheckoutStore { isCompleted: boolean; completeCheckout: () => void; selectedPlan?: Plan; - selectPlan: (plan?: Plan) => void; + selectedPrice?: Price; + selectPlan: (plan?: Plan, price?: Price) => void; pricingInfo?: PricingInfo; updatePrice: (pricingInfo?: PricingInfo) => void; isApplyingCoupon: boolean; @@ -37,6 +38,7 @@ interface ICheckoutStore { export const useCheckoutStore = create((set) => ({ isCompleted: false, selectedPlan: undefined, + selectedPrice: undefined, pricingInfo: undefined, couponCode: undefined, isApplyingCoupon: false, @@ -46,10 +48,11 @@ export const useCheckoutStore = create((set) => ({ state.isCompleted = true; }) ), - selectPlan: (plan) => + selectPlan: (plan, price) => set( produce((state: ICheckoutStore) => { state.selectedPlan = plan; + state.selectedPrice = price; state.pricingInfo = undefined; }) ), diff --git a/apps/web/src/dialogs/buy-dialog/types.ts b/apps/web/src/dialogs/buy-dialog/types.ts index a72a1f31a..31789f7bb 100644 --- a/apps/web/src/dialogs/buy-dialog/types.ts +++ b/apps/web/src/dialogs/buy-dialog/types.ts @@ -17,73 +17,19 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -export type Period = "monthly" | "yearly" | "education"; +import { + CheckoutEventNames, + CheckoutEventsCustomer, + CheckoutEventsDiscount, + CheckoutEventsItem, + PaddleEventData +} from "@paddle/paddle-js"; -export enum PaddleEvents { - /** Checkout has been initialized on the page **/ - "Checkout.Loaded" = "Checkout.Loaded", - /** Checkout has been closed on the page. This is equivalent to when the "closeCallback" checkout parameter is fired . **/ - "Checkout.Close" = "Checkout.Close", - /** Checkout has been completed successfully. This is equivalent to when the "successCallback" checkout parameter is fired . **/ - "Checkout.Complete" = "Checkout.Complete", - /** User has opted into/out of marketing emails in the checkout **/ - "Checkout.User.Subscribed" = "Checkout.User.Subscribed", - /** User has changed the quantity in the checkout **/ - "Checkout.Quantity.Change" = "Checkout.Quantity.Change", - /** User has proceeded past the email checkout step **/ - "Checkout.Login" = "Checkout.Login", - /** User selected 'Not you? Change' in bottom right of checkout **/ - "Checkout.Logout" = "Checkout.Logout", - /** Payment method has been selected **/ - "Checkout.PaymentMethodSelected" = "Checkout.PaymentMethodSelected", - /** User clicked 'Add Coupon' **/ - "Checkout.Coupon.Add" = "Checkout.Coupon.Add", - /** User has submitted a coupon **/ - "Checkout.Coupon.Submit" = "Checkout.Coupon.Submit", - /** User has cancelled the coupon page **/ - "Checkout.Coupon.Cancel" = "Checkout.Coupon.Cancel", - /** Valid coupon applied to purchase **/ - "Checkout.Coupon.Applied" = "Checkout.Coupon.Applied", - /** Coupon has been removed **/ - "Checkout.Coupon.Remove" = "Checkout.Coupon.Remove", - /** Any generic checkout error, like an invalid VAT number or payment failure **/ - "Checkout.Error" = "Checkout.Error", - /** User proceeded past the location page **/ - "Checkout.Location.Submit" = "Checkout.Location.Submit", - /** Language has been changed in the bottom right **/ - "Checkout.Language.Change" = "Checkout.Language.Change", - /** User clicked 'Add VAT Number' **/ - "Checkout.Vat.Add" = "Checkout.Vat.Add", - /** VAT screen cancelled **/ - "Checkout.Vat.Cancel" = "Checkout.Vat.Cancel", - /** VAT number was submitted **/ - "Checkout.Vat.Submit" = "Checkout.Vat.Submit", - /** VAT number was accepted and applied **/ - "Checkout.Vat.Applied" = "Checkout.Vat.Applied", - /** VAT number was removed **/ - "Checkout.Vat.Remove" = "Checkout.Vat.Remove", - /** Wire transfer details have been completed **/ - "Checkout.WireTransfer.Complete" = "Checkout.WireTransfer.Complete", - /** Payment has been completed successfully. **/ - "Checkout.PaymentComplete" = "Checkout.PaymentComplete", - /** User has selected "Change Payment Method" when on the payment screen **/ - "Checkout.PaymentMethodChange" = "Checkout.PaymentMethodChange", - /** User has selected "Change Payment Method" when on the Wire Transfer screen **/ - "Checkout.WireTransfer.PaymentMethodChange" = "Checkout.WireTransfer.PaymentMethodChange", +export type Period = "monthly" | "yearly" | "5-year"; - "Checkout.Customer.Details" = "Checkout.Customer.Details", - "Checkout.RemoveSpinner" = "Checkout.RemoveSpinner" -} - -export interface CallbackData { - checkout?: Checkout; - coupon?: { coupon_code: string }; - user?: { - email: string; - id: string; - country: string; - }; -} +// export interface CallbackData { +// checkout?: Checkout; +// } export interface Checkout { id?: string; @@ -101,29 +47,30 @@ export interface Checkout { export type PaddleEvent = { action: "event"; - event: PaddleEvents; - event_name: PaddleEvents; - callback_data: CallbackData; + event_name: CheckoutEventNames; + callback_data: PaddleEventData; }; +export type PlanId = "free" | "essential" | "pro" | "believer" | "education"; export interface Plan { - id: string; - period: Period; - price: Price; - currency: string; - currencySymbol?: string; - originalPrice: Price; - discount?: Discount; - country: string; + // period: Period; + id: PlanId; + title: string; + prices: Price[]; + recurring: boolean; + // currency: string; + // originalPrice?: Price; + // discount: number; + // country: string; } export type PricingInfo = { country: string; - currency: string; + // currency: string; price: Price; period: Period; recurringPrice: Price; - discount: Discount; + discount?: Discount; coupon?: string; invalidCoupon?: boolean; }; @@ -136,42 +83,13 @@ export type Discount = { }; export interface Price { - gross: number; - gross_after_discount?: number; - net: number; - net_after_discount?: number; - tax: number; - tax_after_discount?: number; - currency?: string; -} - -export interface CheckoutDataResponse { - data: CheckoutData; -} - -export interface CheckoutData { - public_checkout_id: string; - // type: string; - // uuid: string; - // vendor: Vendor; - // display_currency: string; - // charge_currency: string; - // customer: Customer; - items: Item[]; - // available_payment_methods: unknown[]; - // total: TotalPrice[]; - // pending_payment: boolean; - // completed: boolean; - // payment_method_type: null; - // flagged_for_review: boolean; - ip_geo_country_code: string; - // tax: null; - // name: null; - // image_url: null; - // message: null; - // passthrough: string; - // redirect_url: null; - // created_at: Date; + id: string; + period: Period; + subtotal: string; + total: string; + tax: string; + discount?: string; + currency: string; } export interface Customer { @@ -244,4 +162,22 @@ export type TotalPrice = CheckoutPrice & { export interface Vendor { id: number; name: string; + // type: string; + // status: string; + // transaction_id: string; + currency_code: string; + customer: CheckoutEventsCustomer; + // seller: Seller; + items: CheckoutEventsItem[]; + // totals: DataRecurringTotals; + // recurring_totals: DataRecurringTotals; + // payments: Payments; + discount?: CheckoutEventsDiscount; + // is_free: boolean; + // ip_geo_country_code: string; + // custom_data: null; + // created_at: Date; + // environment: string; + // source_page: string; + // messages: any[]; } diff --git a/apps/web/src/dialogs/settings/components/billing-history.tsx b/apps/web/src/dialogs/settings/components/billing-history.tsx index 0e6b93cc8..422f0ffcd 100644 --- a/apps/web/src/dialogs/settings/components/billing-history.tsx +++ b/apps/web/src/dialogs/settings/components/billing-history.tsx @@ -17,19 +17,23 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ +import { useEffect, useState } from "react"; +import { Copy, Loading } from "../../../components/icons"; +import { Box, Button, Link, Flex, Text } from "@theme-ui/components"; import { getFormattedDate } from "@notesnook/common"; import { strings } from "@notesnook/intl"; -import { Box, Flex, Link, Text } from "@theme-ui/components"; -import { useEffect, useState } from "react"; import { db } from "../../../common/db"; import { TransactionStatus, Transaction } from "@notesnook/core"; -import { Loading } from "../../../components/icons"; +import { writeToClipboard } from "../../../utils/clipboard"; +import { showToast } from "../../../utils/toast"; +import { TaskManager } from "../../../common/task-manager"; const TransactionStatusToText: Record = { completed: "Completed", - refunded: "Refunded", - partially_refunded: "Partially refunded", - disputed: "Disputed" + billed: "Billed", + canceled: "Canceled", + paid: "Paid", + past_due: "Past due" }; export function BillingHistory() { @@ -87,11 +91,11 @@ export function BillingHistory() { }} > {[ - { id: "date", title: strings.date(), width: "20%" }, - { id: "orderId", title: strings.orderId(), width: "20%" }, + { id: "id", title: "ID", width: "5%" }, + { id: "billedAt", title: "Billed at", width: "20%" }, { id: "amount", title: strings.amount(), width: "20%" }, { id: "status", title: strings.status(), width: "20%" }, - { id: "receipt", title: strings.receipt(), width: "20%" } + { id: "invoice", title: "Invoice", width: "20%" } ].map((column) => !column.title ? ( @@ -119,29 +123,51 @@ export function BillingHistory() { {transactions.map((transaction) => ( - + - {getFormattedDate(transaction.created_at, "date")} + + writeToClipboard({ "text/plain": transaction.id }) + } + sx={{ cursor: "pointer" }} + /> - {transaction.order_id} + {getFormattedDate(transaction.billed_at, "date")} - {transaction.amount} {transaction.currency} + {(transaction.details.totals.grand_total / 100).toFixed(2)}{" "} + {transaction.details.totals.currency_code} {strings.transactionStatusToText(transaction.status)} - { + const url = await TaskManager.startTask({ + type: "modal", + title: "Getting invoice", + subtitle: "This might take a minute or two.", + action() { + return db.subscriptions.invoice(transaction.id); + } + }); + + if (!url || url instanceof Error) + return showToast( + "error", + url instanceof Error + ? `Failed to get invoice for this transaction: ${url.message}` + : "No invoice found for this transaction." + ); + window.open(url, "_blank"); + }} > - {strings.viewReceipt()} - + Download + ))} @@ -151,28 +177,3 @@ export function BillingHistory() { ); } - -{ - /* - - - Order #{transaction.order_id} - - - - - {transaction.amount} {transaction.currency} - - - - - */ -} diff --git a/apps/web/src/dialogs/settings/components/subscription-status.tsx b/apps/web/src/dialogs/settings/components/subscription-status.tsx index bfd013c75..eec7ec4c1 100644 --- a/apps/web/src/dialogs/settings/components/subscription-status.tsx +++ b/apps/web/src/dialogs/settings/components/subscription-status.tsx @@ -18,69 +18,19 @@ along with this program. If not, see . */ import { useStore as useUserStore } from "../../../stores/user-store"; -import { Button, Flex, Text } from "@theme-ui/components"; -import { useCallback, useMemo, useState } from "react"; -import dayjs from "dayjs"; -import { SUBSCRIPTION_STATUS } from "../../../common/constants"; -import { db } from "../../../common/db"; -import { TaskManager } from "../../../common/task-manager"; -import { showToast } from "../../../utils/toast"; -import { Loading } from "../../../components/icons"; -import { Features } from "../../buy-dialog/features"; -import { ConfirmDialog } from "../../confirm"; -import { BuyDialog } from "../../buy-dialog"; +import { Flex, Text } from "@theme-ui/components"; import { strings } from "@notesnook/intl"; -import { PromptDialog } from "../../prompt"; +import { getSubscriptionInfo } from "./user-profile"; export function SubscriptionStatus() { const user = useUserStore((store) => store.user); - const [activateTrial, isActivatingTrial] = useAction(async () => { - await db.user.activateTrial(); - }); + const { title, autoRenew, expiryDate, trial, legacy } = + getSubscriptionInfo(user); - const provider = - strings.subscriptionProviderInfo[user?.subscription?.provider || 0]; - const { - isTrial, - isBeta, - isPro, - isBasic, - isProCancelled, - isProExpired, - remainingDays - } = useMemo(() => { - const type = user?.subscription?.type; - const expiry = user?.subscription?.expiry; - if (!expiry) return { isBasic: true, remainingDays: 0 }; - return { - remainingDays: dayjs(expiry).diff(dayjs(), "day"), - isTrial: type === SUBSCRIPTION_STATUS.TRIAL, - isBasic: type === SUBSCRIPTION_STATUS.BASIC, - isBeta: type === SUBSCRIPTION_STATUS.BETA, - isPro: type === SUBSCRIPTION_STATUS.PREMIUM, - isProCancelled: type === SUBSCRIPTION_STATUS.PREMIUM_CANCELED, - isProExpired: type === SUBSCRIPTION_STATUS.PREMIUM_EXPIRED - }; - }, [user]); - - const subtitle = useMemo(() => { - const expiryDate = dayjs(user?.subscription?.expiry).format("MMMM D, YYYY"); - const startDate = dayjs(user?.subscription?.start).format("MMMM D, YYYY"); - return isPro - ? provider.type === "Streetwriters" || provider.type === "Gift card" - ? `Ending on ${expiryDate}` - : `Next payment on ${expiryDate}.` - : isProCancelled - ? `Ending on ${expiryDate}.` - : isProExpired - ? "Your account will be downgraded to Basic in 3 days." - : isBeta - ? `Beta member since ${startDate}` - : isTrial - ? `Ending on ${expiryDate}` - : null; - }, [isPro, isProExpired, isProCancelled, isBeta, isTrial, user, provider]); + const subtitle = autoRenew + ? `Your subscription will auto renew on ${expiryDate}.` + : `Your account will automatically downgrade to the Free plan on ${expiryDate}.`; if (!user) return null; return ( @@ -92,8 +42,8 @@ export function SubscriptionStatus() { justifyContent: "center", alignItems: "start", bg: "var(--background-secondary)", - p: 2, - mb: isBasic ? 0 : 4 + p: 2 + // mb: isBasic ? 0 : 4 }} > - {remainingDays > 0 && (isPro || isProCancelled) - ? `Pro` - : remainingDays > 0 && isTrial - ? "Trial" - : isBeta - ? "Beta user" - : "Basic"} + {title} + {legacy ? " (legacy)" : ""} - {remainingDays > 0 && (isPro || isProCancelled || isTrial || isBeta) - ? `Access to all Pro features including unlimited storage for attachments, - notebooks & tags.` - : "Access only to basic features including unlimited notes & end-to-end encrypted syncing to unlimited devices."} + {trial ? "Your free trial is on-going." : subtitle} - {remainingDays > 0 && (isPro || isProCancelled) ? ( - - {subtitle}. {provider.desc()} - - ) : null} - - {provider.type === "Web" && (isPro || isProCancelled) ? ( - <> - {isPro && ( - - )} - - - ) : null} - {!isPro && ( - <> - - {isBasic && ( - - )} - - - )} - - {isBasic ? : null} + {/* {isBasic ? : null} */} ); } - -function useAction(action: () => Promise) { - const [isLoading, setIsLoading] = useState(false); - - const _action = useCallback(async () => { - try { - setIsLoading(true); - await action(); - } catch (e) { - if (e instanceof Error) { - showToast("error", e.message); - } - } finally { - setIsLoading(false); - } - }, [action]); - - return [_action, isLoading] as const; -} diff --git a/apps/web/src/dialogs/settings/components/user-profile.tsx b/apps/web/src/dialogs/settings/components/user-profile.tsx index dd76c0e72..7563406b3 100644 --- a/apps/web/src/dialogs/settings/components/user-profile.tsx +++ b/apps/web/src/dialogs/settings/components/user-profile.tsx @@ -18,19 +18,79 @@ along with this program. If not, see . */ import { Flex, Image, Text } from "@theme-ui/components"; -import { Edit, User } from "../../../components/icons"; +import { Edit, User as UserIcon } from "../../../components/icons"; import { useStore as useUserStore } from "../../../stores/user-store"; import { useStore as useSettingStore } from "../../../stores/setting-store"; import { getObjectIdTimestamp } from "@notesnook/core"; import { getFormattedDate } from "@notesnook/common"; -import { SUBSCRIPTION_STATUS } from "../../../common/constants"; -import dayjs from "dayjs"; -import { useMemo } from "react"; import { db } from "../../../common/db"; import { showToast } from "../../../utils/toast"; import { EditProfilePictureDialog } from "../../edit-profile-picture-dialog"; import { PromptDialog } from "../../prompt"; import { strings } from "@notesnook/intl"; +import { + SubscriptionPlan, + SubscriptionProvider, + SubscriptionStatus, + SubscriptionType, + User +} from "@notesnook/core"; + +export function getSubscriptionInfo(user: User | undefined): { + title: string; + trial?: boolean; + paused?: boolean; + canceled?: boolean; + legacy?: boolean; + expiryDate?: string; + startDate?: string; + autoRenew?: boolean; +} { + const { type, expiry, plan, status, provider } = user?.subscription || {}; + if (!expiry) return { title: "FREE" }; + + const legacy = !!type; + const trial = + status === SubscriptionStatus.TRIAL || type === SubscriptionType.TRIAL; + const title = + plan === SubscriptionPlan.BELIEVER + ? "BELIEVER" + : plan === SubscriptionPlan.PRO || + type === SubscriptionType.PREMIUM || + type === SubscriptionType.PREMIUM_CANCELED + ? "PRO" + : plan === SubscriptionPlan.ESSENTIAL + ? "ESSENTIAL" + : plan === SubscriptionPlan.EDUCATION + ? "EDUCATION" + : "FREE"; + const autoRenew = + (status === SubscriptionStatus.ACTIVE || + status === SubscriptionStatus.TRIAL) && + provider !== SubscriptionProvider.STREETWRITERS; + const paused = status === SubscriptionStatus.PAUSED; + const canceled = status === SubscriptionStatus.CANCELED; + + const expiryDate = + (!!user?.subscription?.expiry && + getFormattedDate(user?.subscription?.expiry, "date-time")) || + undefined; + const startDate = + (!!user?.subscription?.start && + getFormattedDate(user?.subscription?.start, "date-time")) || + undefined; + + return { + title, + legacy, + trial, + expiryDate, + startDate, + autoRenew, + paused, + canceled + }; +} type Props = { minimal?: boolean; @@ -40,28 +100,7 @@ export function UserProfile({ minimal }: Props) { const user = useUserStore((store) => store.user); const profile = useSettingStore((store) => store.profile); - const { - isTrial, - isBeta, - isPro, - isBasic, - isProCancelled, - isProExpired, - remainingDays - } = useMemo(() => { - const type = user?.subscription?.type; - const expiry = user?.subscription?.expiry; - if (!expiry) return { isBasic: true, remainingDays: 0 }; - return { - remainingDays: dayjs(expiry).diff(dayjs(), "day"), - isTrial: type === SUBSCRIPTION_STATUS.TRIAL, - isBasic: type === SUBSCRIPTION_STATUS.BASIC, - isBeta: type === SUBSCRIPTION_STATUS.BETA, - isPro: type === SUBSCRIPTION_STATUS.PREMIUM, - isProCancelled: type === SUBSCRIPTION_STATUS.PREMIUM_CANCELED, - isProExpired: type === SUBSCRIPTION_STATUS.PREMIUM_EXPIRED - }; - }, [user]); + const { title, legacy, trial } = getSubscriptionInfo(user); if (!user || !user.id) return ( @@ -83,7 +122,7 @@ export function UserProfile({ minimal }: Props) { borderRadius: 80 }} > - + @@ -126,7 +165,7 @@ export function UserProfile({ minimal }: Props) { src={profile.profilePicture} /> ) : ( - + )} - {remainingDays > 0 && (isPro || isProCancelled) - ? `PRO` - : remainingDays > 0 && isTrial - ? "TRIAL" - : isBeta - ? "BETA TESTER" - : "BASIC"} + {`${title}${trial ? " (trial)" : ""}${legacy ? " (legacy)" : ""}`} diff --git a/apps/web/src/dialogs/settings/subscription-settings.ts b/apps/web/src/dialogs/settings/subscription-settings.ts index 93d33bad4..eccb67cda 100644 --- a/apps/web/src/dialogs/settings/subscription-settings.ts +++ b/apps/web/src/dialogs/settings/subscription-settings.ts @@ -25,6 +25,13 @@ import { BillingHistory } from "./components/billing-history"; import { useStore as useUserStore } from "../../stores/user-store"; import { isUserSubscribed } from "../../hooks/use-is-user-premium"; import { strings } from "@notesnook/intl"; +import { + SubscriptionPlan, + SubscriptionProvider, + SubscriptionStatus as SubscriptionStatusEnum +} from "@notesnook/core"; +import { TaskManager } from "../../common/task-manager"; +import { ConfirmDialog } from "../confirm"; export const SubscriptionSettings: SettingsGroup[] = [ { @@ -32,13 +39,59 @@ export const SubscriptionSettings: SettingsGroup[] = [ section: "subscription", header: SubscriptionStatus, settings: [ + { + key: "auto-renew", + title: "Auto renew", + onStateChange: (listener) => + useUserStore.subscribe((s) => s.user, listener), + description: + "Toggle auto renew to avoid any surprise charges. If you do not turn auto renew back on, you'll be automatically downgraded to the Free plan at the end of your billing period.", + isHidden: () => { + const user = useUserStore.getState().user; + const status = user?.subscription.status; + return ( + user?.subscription.provider !== SubscriptionProvider.PADDLE || + !isUserSubscribed(user) || + status === SubscriptionStatusEnum.CANCELED || + status === SubscriptionStatusEnum.EXPIRED + ); + }, + components: [ + { + type: "toggle", + isToggled: () => + useUserStore.getState().user?.subscription.status === + SubscriptionStatusEnum.ACTIVE, + async toggle() { + try { + const user = useUserStore.getState().user; + const status = user?.subscription.status; + if (status === SubscriptionStatusEnum.ACTIVE) + await db.subscriptions.pause(); + else await db.subscriptions.resume(); + useUserStore.setState((state) => { + state.user!.subscription.status = + status === SubscriptionStatusEnum.ACTIVE + ? SubscriptionStatusEnum.PAUSED + : SubscriptionStatusEnum.ACTIVE; + }); + } catch (e) { + showToast("error", (e as Error).message); + } + } + } + ] + }, { key: "payment-method", title: strings.paymentMethod(), description: strings.changePaymentMethodDescription(), isHidden: () => { const user = useUserStore.getState().user; - return !isUserSubscribed(user) || user?.subscription.provider !== 3; + return ( + user?.subscription.provider !== SubscriptionProvider.PADDLE || + !isUserSubscribed(user) + ); }, components: [ { @@ -46,7 +99,12 @@ export const SubscriptionSettings: SettingsGroup[] = [ title: strings.update(), action: async () => { try { - window.open(await db.subscriptions.updateUrl(), "_blank"); + const urls = await db.subscriptions.urls(); + if (!urls) + throw new Error( + "Failed to get subscription management urls. Please contact us at support@streetwriters.co so we can help you update your payment method." + ); + window.open(urls?.update_payment_method, "_blank"); } catch (e) { if (e instanceof Error) showToast("error", e.message); } @@ -55,12 +113,108 @@ export const SubscriptionSettings: SettingsGroup[] = [ } ] }, + { + key: "cancel-subscription", + title: "Cancel subscription", + onStateChange: (listener) => + useUserStore.subscribe((s) => s.user, listener), + description: `Cancel your subscription to stop all future charges permanently. You will automatically be downgraded to the Free plan at the end of your billing period. + +Canceled subscriptions cannot be resumed/renewed which is why it is recommended that you disable auto renew instead.`, + isHidden: () => { + const user = useUserStore.getState().user; + const status = user?.subscription.status; + return ( + user?.subscription.provider !== SubscriptionProvider.PADDLE || + !isUserSubscribed(user) || + status === SubscriptionStatusEnum.CANCELED || + status === SubscriptionStatusEnum.EXPIRED + ); + }, + components: [ + { + type: "button", + title: "Cancel", + async action() { + const cancelSubscription = await ConfirmDialog.show({ + title: "Cancel subscription?", + message: + "Cancel your subscription to stop all future charges permanently. You will automatically be downgraded to the Free plan at the end of your billing period.", + negativeButtonText: "No", + positiveButtonText: "Yes" + }); + if (cancelSubscription) { + await TaskManager.startTask({ + type: "modal", + title: "Cancelling your subscription", + subtitle: "Please wait...", + action: () => db.subscriptions.cancel() + }) + .catch((e) => showToast("error", e.message)) + .then(() => + showToast("success", "Your subscription has been canceled.") + ); + } + }, + variant: "error" + } + ] + }, + { + key: "refund-subscription", + title: "Refund subscription", + onStateChange: (listener) => + useUserStore.subscribe((s) => s.user, listener), + description: `You will only be issued a refund if you are eligible as per our refund policy. Your account will immediately be downgraded to Basic and your funds will be transferred to your account within 24 hours.`, + isHidden: () => { + const user = useUserStore.getState().user; + return ( + user?.subscription.provider !== SubscriptionProvider.PADDLE || + !isUserSubscribed(user) || + user.subscription.plan === SubscriptionPlan.EDUCATION + ); + }, + components: [ + { + type: "button", + title: "Refund", + async action() { + const refundSubscription = await ConfirmDialog.show({ + title: "Request refund?", + message: + "You will only be issued a refund if you are eligible as per our refund policy. Your account will immediately be downgraded to Basic and your funds will be transferred to your account within 24 hours.", + negativeButtonText: "No", + positiveButtonText: "Yes" + }); + if (refundSubscription) { + await TaskManager.startTask({ + type: "modal", + title: "Requesting refund for your subscription", + subtitle: "Please wait...", + action: () => db.subscriptions.refund() + }) + .catch((e) => showToast("error", e.message)) + .then(() => + showToast( + "success", + "Your refund request has been sent. If you are eligible for a refund, you'll receive your funds within 24 hours. Please wait at least 24 hours before reaching out to us in case there is any problem." + ) + ); + } + }, + variant: "error" + } + ] + }, { key: "billing-history", title: strings.billingHistory(), isHidden: () => { const user = useUserStore.getState().user; - return !isUserSubscribed(user) || user?.subscription.provider !== 3; + return ( + user?.subscription.provider !== SubscriptionProvider.PADDLE || + !isUserSubscribed(user) + ); }, components: [{ type: "custom", component: BillingHistory }] } diff --git a/apps/web/src/global.d.ts b/apps/web/src/global.d.ts index 508c474aa..03b6a67e1 100644 --- a/apps/web/src/global.d.ts +++ b/apps/web/src/global.d.ts @@ -34,6 +34,10 @@ declare global { var IS_THEME_BUILDER: boolean; var hasNativeTitlebar: boolean; + interface Window { + ApplePaySession?: PaymentRequest; + } + interface AuthenticationExtensionsClientInputs { prf?: { eval: { diff --git a/apps/web/src/hooks/use-is-user-premium.ts b/apps/web/src/hooks/use-is-user-premium.ts index 533e09db1..d805e9c66 100644 --- a/apps/web/src/hooks/use-is-user-premium.ts +++ b/apps/web/src/hooks/use-is-user-premium.ts @@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -import { User } from "@notesnook/core"; +import { SubscriptionPlan, SubscriptionType, User } from "@notesnook/core"; import { SUBSCRIPTION_STATUS } from "../common/constants"; import { useStore as useUserStore, @@ -47,9 +47,10 @@ export function isUserSubscribed(user?: User) { if (!user) user = userstore.get().user; if (!user) return false; - const subStatus = user.subscription?.type; + const { type, plan } = user.subscription || {}; return ( - subStatus === SUBSCRIPTION_STATUS.PREMIUM || - subStatus === SUBSCRIPTION_STATUS.PREMIUM_CANCELED + (type === SubscriptionType.PREMIUM || + type === SubscriptionType.PREMIUM_CANCELED) && + plan !== SubscriptionPlan.FREE ); } diff --git a/apps/web/src/views/payments.tsx b/apps/web/src/views/payments.tsx new file mode 100644 index 000000000..2de44753e --- /dev/null +++ b/apps/web/src/views/payments.tsx @@ -0,0 +1,61 @@ +/* +This file is part of the Notesnook project (https://notesnook.com/) + +Copyright (C) 2023 Streetwriters (Private) Limited + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +import "../app.css"; +import { useEffect, useState } from "react"; +import { Flex } from "@theme-ui/components"; +import { hardNavigate, useQueryParams } from "../navigation"; +import { initializePaddle } from "@paddle/paddle-js"; +import { CLIENT_PADDLE_TOKEN } from "../dialogs/buy-dialog/paddle"; +import { Loader } from "../components/loader"; +import { IS_DEV } from "../dialogs/buy-dialog/helpers"; + +function Payments() { + const [{ _ptxn }] = useQueryParams(); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + if (!_ptxn) return hardNavigate("/notes"); + (async function () { + const paddle = await initializePaddle({ + token: CLIENT_PADDLE_TOKEN, + environment: IS_DEV ? "sandbox" : "production" + }); + setIsLoading(false); + paddle?.Checkout.open({ + transactionId: _ptxn, + settings: { displayMode: "overlay" } + }); + })(); + }, [_ptxn]); + + return isLoading ? ( + + + + ) : null; +} +export default Payments;