From ef0e1a81b912c41ad275b0b99ae4ba605f8c49d8 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 18 Jul 2024 10:56:27 -0500 Subject: [PATCH] feat(web): license UI (#11182) --- .dockerignore | 1 + .../2024/immich-core-team-goes-fulltime.mdx | 2 +- docs/blog/2024/immich-licensing.mdx | 91 +++++++++ e2e/package-lock.json | 6 +- e2e/src/api/specs/asset.e2e-spec.ts | 2 +- server/src/controllers/server.controller.ts | 2 +- server/src/emails/license.email.tsx | 186 ++++++++++++++++++ server/src/services/server.service.ts | 2 +- web/package-lock.json | 13 ++ web/package.json | 1 + web/src/app.d.ts | 5 + .../full-screen-modal.svelte | 2 +- .../license/license-activation-success.svelte | 18 ++ .../license/license-content.svelte | 70 +++++++ .../license/license-modal.svelte | 25 +++ .../license/server-license-card.svelte | 44 +++++ .../license/user-license-card.svelte | 39 ++++ .../navigation-bar/navigation-bar.svelte | 1 + .../shared-components/portal/portal.svelte | 18 ++ .../side-bar/admin-side-bar.svelte | 6 +- .../side-bar/bottom-info.svelte | 17 ++ .../side-bar/license-info.svelte | 92 +++++++++ .../side-bar/server-status.svelte | 49 +++++ .../side-bar/side-bar.svelte | 7 +- .../side-bar/storage-space.svelte | 82 ++++++++ .../shared-components/status-box.svelte | 125 ------------ .../license-settings.svelte | 158 +++++++++++++++ .../user-settings-list.svelte | 9 + web/src/lib/constants.ts | 6 + web/src/lib/i18n/en.json | 33 +++- web/src/lib/stores/license.store.ts | 18 ++ web/src/lib/stores/user.store.ts | 2 + web/src/lib/utils/auth.ts | 27 ++- web/src/lib/utils/license-utils.ts | 26 +++ web/src/routes/(user)/buy/+page.svelte | 53 +++++ web/src/routes/(user)/buy/+page.ts | 38 ++++ web/src/routes/+layout.svelte | 1 - web/src/routes/link/+page.ts | 22 ++- web/svelte.config.js | 6 + 39 files changed, 1157 insertions(+), 148 deletions(-) create mode 100644 docs/blog/2024/immich-licensing.mdx create mode 100644 server/src/emails/license.email.tsx create mode 100644 web/src/lib/components/shared-components/license/license-activation-success.svelte create mode 100644 web/src/lib/components/shared-components/license/license-content.svelte create mode 100644 web/src/lib/components/shared-components/license/license-modal.svelte create mode 100644 web/src/lib/components/shared-components/license/server-license-card.svelte create mode 100644 web/src/lib/components/shared-components/license/user-license-card.svelte create mode 100644 web/src/lib/components/shared-components/side-bar/bottom-info.svelte create mode 100644 web/src/lib/components/shared-components/side-bar/license-info.svelte create mode 100644 web/src/lib/components/shared-components/side-bar/server-status.svelte create mode 100644 web/src/lib/components/shared-components/side-bar/storage-space.svelte delete mode 100644 web/src/lib/components/shared-components/status-box.svelte create mode 100644 web/src/lib/components/user-settings-page/license-settings.svelte create mode 100644 web/src/lib/stores/license.store.ts create mode 100644 web/src/lib/utils/license-utils.ts create mode 100644 web/src/routes/(user)/buy/+page.svelte create mode 100644 web/src/routes/(user)/buy/+page.ts diff --git a/.dockerignore b/.dockerignore index 7559cf366a..a3096e7d40 100644 --- a/.dockerignore +++ b/.dockerignore @@ -29,3 +29,4 @@ web/node_modules/ web/coverage/ web/.svelte-kit web/build/ +web/.env diff --git a/docs/blog/2024/immich-core-team-goes-fulltime.mdx b/docs/blog/2024/immich-core-team-goes-fulltime.mdx index 5edd39ad78..0cba2b467c 100644 --- a/docs/blog/2024/immich-core-team-goes-fulltime.mdx +++ b/docs/blog/2024/immich-core-team-goes-fulltime.mdx @@ -1,7 +1,7 @@ --- title: The Immich core team goes full-time authors: [alextran] -tags: [update, announcement, futo] +tags: [update, announcement, FUTO] date: 2024-05-01T00:00 --- diff --git a/docs/blog/2024/immich-licensing.mdx b/docs/blog/2024/immich-licensing.mdx new file mode 100644 index 0000000000..4b4272e163 --- /dev/null +++ b/docs/blog/2024/immich-licensing.mdx @@ -0,0 +1,91 @@ +--- +title: Licensing announcement - Purchase a license to support Immich +authors: [alextran] +tags: [update, announcement, FUTO] +date: 2024-07-18T00:00 +--- + +Hello everybody, + +Firstly, on behalf of the Immich team, I'd like to thank everybody for your continuous support of Immich since the very first day! Your contributions, encouragement, and community engagement have helped bring Immich to its current state. The team and I are forever grateful for that. + +Since our [last announcement of the core team joining FUTO to work on Immich full-time](https://immich.app/blog/2024/immich-core-team-goes-fulltime), one of the goals of our new position is to foster a healthy relationship between the developers and the users. We believe that this enables us to create great software, establish transparent policies and build trust. + +We want to build a great software application that brings value to you and your loved ones' lives. We are not using you as a product, i.e., selling or tracking your data. We are not putting annoying ads into our software. We respect your privacy. We want to be compensated for the hard work we put in to build Immich for you. + +With those notes, we have enabled a way for you to financially support the continued development of Immich, ensuring the software can move forward and will be maintained, by offering a lifetime license of the software. We think if you like and use software, you should pay for it, but _we're never going to force anyone to pay or try to limit Immich for those who don't._ + +There are two types of license that you can choose to purchase: **Server License** and **Individual License**. + +### Server License + +This is a lifetime license costing **$99.99**. The license is applied to the whole server. You and all users that use your server are licensed. + +### Individual License + +This is a lifetime license costing **$24.99**. The license is applied to a single user, and can be used on any server they choose to connect to. + +license-social-gh + +You can purchase the license on [our page - https://buy.immich.app](https://buy.immich.app). + +Starting with release `v1.109.0` you can purchase and enter your purchased license key directly in the app. + +license-page-gh + +## Thank you + +Thank you again for your support, this will help create a strong foundation and stability for the Immich team to continue developing and maintaining the project that you love to use. + +

+ +

+ +
+
+ +Cheers! 🎉 + +Immich team + +# FAQ + +### 1. Where can I purchase a license? + +There are several places where you can purchase the license from + +- [https://buy.immich.app](https://buy.immich.app) +- [https://pay.futo.org](https://pay.futo.org/) +- or directly from the app. + +### 2. Do I need both _Individual License_ and _Server License_? + +No, + +If you are the admin and the sole user, or your instance has less than a total of 4 users, you can buy the **Individual License** for each user. + +If your instance has more than 4 users, it is more cost-effective to buy the **Server License**, which will license all the users on your instance. + +### 3. What do I do if I don't pay? + +You can continue using Immich for an unlimited trial period. + +### 4. Will there be any paywalled features? + +No, there will never be any paywalled features. + +### 5. Where can I get support regarding payment issues? + +You can email us with your `orderId` and your email address `billing@futo.org` or on our Discord server. diff --git a/e2e/package-lock.json b/e2e/package-lock.json index 9a57519d2d..1c6e65cf84 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -56,12 +56,12 @@ "devDependencies": { "@immich/sdk": "file:../open-api/typescript-sdk", "@types/byte-size": "^8.1.0", - "@types/cli-progress": "^3.11.6", + "@types/cli-progress": "^3.11.0", "@types/lodash-es": "^4.17.12", "@types/mock-fs": "^4.13.1", "@types/node": "^20.14.10", - "@typescript-eslint/eslint-plugin": "^7.16.0", - "@typescript-eslint/parser": "^7.16.0", + "@typescript-eslint/eslint-plugin": "^7.0.0", + "@typescript-eslint/parser": "^7.0.0", "@vitest/coverage-v8": "^1.2.2", "byte-size": "^8.1.1", "cli-progress": "^3.12.0", diff --git a/e2e/src/api/specs/asset.e2e-spec.ts b/e2e/src/api/specs/asset.e2e-spec.ts index 694114aed5..a5ba40e148 100644 --- a/e2e/src/api/specs/asset.e2e-spec.ts +++ b/e2e/src/api/specs/asset.e2e-spec.ts @@ -507,7 +507,7 @@ describe('/asset', () => { expect(status).toEqual(200); }); - it('should geocode country from gps data in the middle of nowhere', async () => { + it.skip('should geocode country from gps data in the middle of nowhere', async () => { const { status } = await request(app) .put(`/assets/${user1Assets[0].id}`) .set('Authorization', `Bearer ${user1.accessToken}`) diff --git a/server/src/controllers/server.controller.ts b/server/src/controllers/server.controller.ts index b98ca38a80..009c36c793 100644 --- a/server/src/controllers/server.controller.ts +++ b/server/src/controllers/server.controller.ts @@ -95,7 +95,7 @@ export class ServerController { @Get('license') @Authenticated({ admin: true }) - getServerLicense(): Promise { + getServerLicense(): Promise { return this.service.getLicense(); } } diff --git a/server/src/emails/license.email.tsx b/server/src/emails/license.email.tsx new file mode 100644 index 0000000000..9c6c42a152 --- /dev/null +++ b/server/src/emails/license.email.tsx @@ -0,0 +1,186 @@ +import { + Body, + Button, + Column, + Container, + Head, + Hr, + Html, + Img, + Link, + Preview, + Row, + Section, + Text, +} from '@react-email/components'; +import * as CSS from 'csstype'; +import * as React from 'react'; + +/** + * Template to be used for FUTOPay project + * Variable is {{LICENSEKEY}} + * */ +export const LicenseEmail = () => ( + + + Your Immich Server License + + +
+ Immich + + Thank you for supporting Immich and open-source software + + + Your Immich license key is + + +
+ + {'{{LICENSEKEY}}'} + +
+ + {/* + To activate your instance, you can click the following button or copy and paste the link below to your + browser + + + + + + + + + + + + https://my.immich.app/link?target=activate_license&licenseKey={'{{LICENSEKEY}}'}&activationKey= + {'{{ACTIVATIONKEY}}'} + + + */} +
+ +
+ + + + FUTO + + + +
+ +
+ +
+ + + Immich + + + Immich + + +
+ + + Immich project is available under GNU AGPL v3 license. + +
+ + +); + +LicenseEmail.PreviewProps = {}; + +export default LicenseEmail; + +const text = { + margin: '0 0 24px 0', + textAlign: 'left' as const, + fontSize: '16px', + lineHeight: '24px', +}; + +const button: CSS.Properties = { + backgroundColor: 'rgb(66, 80, 175)', + margin: '1em 0', + padding: '0.75em 3em', + color: '#fff', + fontSize: '1em', + fontWeight: 600, + lineHeight: 1.5, + textTransform: 'uppercase', + borderRadius: '9999px', +}; diff --git a/server/src/services/server.service.ts b/server/src/services/server.service.ts index b477f0f35c..1aaf85b1ba 100644 --- a/server/src/services/server.service.ts +++ b/server/src/services/server.service.ts @@ -164,7 +164,7 @@ export class ServerService implements OnEvents { await this.systemMetadataRepository.delete(SystemMetadataKey.LICENSE); } - async getLicense(): Promise { + async getLicense(): Promise { return this.systemMetadataRepository.get(SystemMetadataKey.LICENSE); } diff --git a/web/package-lock.json b/web/package-lock.json index 02513f110a..40bb5afb5f 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -47,6 +47,7 @@ "@typescript-eslint/parser": "^7.1.0", "@vitest/coverage-v8": "^1.3.1", "autoprefixer": "^10.4.17", + "dotenv": "^16.4.5", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-svelte": "^2.35.1", @@ -3740,6 +3741,18 @@ "resolved": "https://registry.npmjs.org/dom-to-image/-/dom-to-image-2.6.0.tgz", "integrity": "sha512-Dt0QdaHmLpjURjU7Tnu3AgYSF2LuOmksSGsUcE6ItvJoCWTBEmiMXcqBdNSAm9+QbbwD7JMoVsuuKX6ZVQv1qA==" }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/earcut": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz", diff --git a/web/package.json b/web/package.json index d202fa3a39..a2a1a91241 100644 --- a/web/package.json +++ b/web/package.json @@ -40,6 +40,7 @@ "@typescript-eslint/parser": "^7.1.0", "@vitest/coverage-v8": "^1.3.1", "autoprefixer": "^10.4.17", + "dotenv": "^16.4.5", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-svelte": "^2.35.1", diff --git a/web/src/app.d.ts b/web/src/app.d.ts index 241a579fc7..ae6c5b559b 100644 --- a/web/src/app.d.ts +++ b/web/src/app.d.ts @@ -27,3 +27,8 @@ declare namespace svelteHTML { 'on:zoomImage'?: () => void; } } + +declare module '$env/static/public' { + export const PUBLIC_IMMICH_PAY_HOST: string; + export const PUBLIC_IMMICH_BUY_HOST: string; +} diff --git a/web/src/lib/components/shared-components/full-screen-modal.svelte b/web/src/lib/components/shared-components/full-screen-modal.svelte index bc1253a546..be407decde 100644 --- a/web/src/lib/components/shared-components/full-screen-modal.svelte +++ b/web/src/lib/components/shared-components/full-screen-modal.svelte @@ -39,7 +39,7 @@ } else if (width === 'narrow') { modalWidth = 'w-[28rem]'; } else { - modalWidth = 'sm:max-w-lg'; + modalWidth = 'sm:max-w-4xl'; } } diff --git a/web/src/lib/components/shared-components/license/license-activation-success.svelte b/web/src/lib/components/shared-components/license/license-activation-success.svelte new file mode 100644 index 0000000000..f77e854aec --- /dev/null +++ b/web/src/lib/components/shared-components/license/license-activation-success.svelte @@ -0,0 +1,18 @@ + + +
+ +

{$t('license_activated_title')}

+

{$t('license_activated_subtitle')}

+ +
+ +
+
diff --git a/web/src/lib/components/shared-components/license/license-content.svelte b/web/src/lib/components/shared-components/license/license-content.svelte new file mode 100644 index 0000000000..e5f780265d --- /dev/null +++ b/web/src/lib/components/shared-components/license/license-content.svelte @@ -0,0 +1,70 @@ + + +
+
+

+ {$t('license_license_title')} +

+

{$t('license_license_subtitle')}

+
+
+ {#if $user.isAdmin} + + {/if} + +
+ +
+

{$t('license_input_suggestion')}

+
+ + +
+
+
diff --git a/web/src/lib/components/shared-components/license/license-modal.svelte b/web/src/lib/components/shared-components/license/license-modal.svelte new file mode 100644 index 0000000000..9f7e23c5d1 --- /dev/null +++ b/web/src/lib/components/shared-components/license/license-modal.svelte @@ -0,0 +1,25 @@ + + + + + {#if showLicenseActivated} + + {:else} + { + showLicenseActivated = true; + }} + /> + {/if} + + diff --git a/web/src/lib/components/shared-components/license/server-license-card.svelte b/web/src/lib/components/shared-components/license/server-license-card.svelte new file mode 100644 index 0000000000..bfdbb3a665 --- /dev/null +++ b/web/src/lib/components/shared-components/license/server-license-card.svelte @@ -0,0 +1,44 @@ + + + +
+
+ +

{$t('license_server_title')}

+
+ +
+

$99.99

+

{$t('license_per_server')}

+
+ +
+
+
+ +

{$t('license_server_description_1')}

+
+ +
+ +

{$t('license_lifetime_description')}

+
+ +
+ +

{$t('license_server_description_2')}

+
+
+ + + + +
+
diff --git a/web/src/lib/components/shared-components/license/user-license-card.svelte b/web/src/lib/components/shared-components/license/user-license-card.svelte new file mode 100644 index 0000000000..96f30c6857 --- /dev/null +++ b/web/src/lib/components/shared-components/license/user-license-card.svelte @@ -0,0 +1,39 @@ + + + +
+
+ +

{$t('license_individual_title')}

+
+ +
+

$24.99

+

{$t('license_per_user')}

+
+ +
+
+
+ +

{$t('license_individual_description_1')}

+
+ +
+ +

{$t('license_lifetime_description')}

+
+
+ + + + +
+
diff --git a/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte b/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte index c3726c967e..e0c8ff7457 100644 --- a/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte +++ b/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte @@ -31,6 +31,7 @@ const logOut = async () => { const { redirectUri } = await logout(); + if (redirectUri.startsWith('/')) { await goto(redirectUri); } else { diff --git a/web/src/lib/components/shared-components/portal/portal.svelte b/web/src/lib/components/shared-components/portal/portal.svelte index 924e5f0c6b..7a9e577083 100644 --- a/web/src/lib/components/shared-components/portal/portal.svelte +++ b/web/src/lib/components/shared-components/portal/portal.svelte @@ -45,6 +45,24 @@ } + + +
+ +
+ +
+ +
+ +
+ +
diff --git a/web/src/lib/components/shared-components/side-bar/license-info.svelte b/web/src/lib/components/shared-components/side-bar/license-info.svelte new file mode 100644 index 0000000000..62e793a27f --- /dev/null +++ b/web/src/lib/components/shared-components/side-bar/license-info.svelte @@ -0,0 +1,92 @@ + + +{#if isOpen} + (isOpen = false)} /> +{/if} + + + + + {#if showMessage && getAccountAge() > 14} +
+
+ + { + showMessage = false; + }} + title="Close" + size="18" + class="text-immich-dark-gray/85 dark:text-immich-gray" + /> +
+

{$t('license_trial_info_1')}

+

+ {$t('license_trial_info_2')} + + {$t('license_trial_info_3', { values: { accountAge: getAccountAge() } })}. {$t('license_trial_info_4')} +

+
+ +
+
+ {/if} +
diff --git a/web/src/lib/components/shared-components/side-bar/server-status.svelte b/web/src/lib/components/shared-components/side-bar/server-status.svelte new file mode 100644 index 0000000000..83ed98584a --- /dev/null +++ b/web/src/lib/components/shared-components/side-bar/server-status.svelte @@ -0,0 +1,49 @@ + + +{#if isOpen} + (isOpen = false)} info={aboutInfo} /> +{/if} + +