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 7559cf366a480..a3096e7d40883 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 5edd39ad78423..0cba2b467c07f 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 0000000000000..4b4272e163364 --- /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 9a57519d2d1c6..1c6e65cf84d34 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 694114aed596b..a5ba40e148be8 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 b98ca38a80106..009c36c793470 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 0000000000000..9c6c42a1523c6 --- /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 b477f0f35cd88..1aaf85b1ba64f 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 02513f110a136..40bb5afb5f6fa 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 d202fa3a3930e..a2a1a912415b9 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 241a579fc729b..ae6c5b559bb8f 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 bc1253a546c01..be407decded14 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 0000000000000..f77e854aec47d --- /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 0000000000000..e5f780265d6c4 --- /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 0000000000000..9f7e23c5d1db0 --- /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 0000000000000..bfdbb3a665088 --- /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 0000000000000..96f30c68578aa --- /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 c3726c967e10e..e0c8ff7457f08 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 924e5f0c6ba9b..7a9e577083015 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 0000000000000..62e793a27f58b --- /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 0000000000000..83ed98584ab13 --- /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} + +