diff --git a/.dockerignore b/.dockerignore index 7559cf366..a3096e7d4 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 5edd39ad7..0cba2b467 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 000000000..4b4272e16 --- /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 9a57519d2..1c6e65cf8 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 694114aed..a5ba40e14 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 b98ca38a8..009c36c79 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 000000000..9c6c42a15 --- /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 b477f0f35..1aaf85b1b 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 02513f110..40bb5afb5 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 d202fa3a3..a2a1a9124 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 241a579fc..ae6c5b559 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 bc1253a54..be407decd 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 000000000..f77e854ae --- /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 000000000..e5f780265 --- /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 000000000..9f7e23c5d --- /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 000000000..bfdbb3a66 --- /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 000000000..96f30c685 --- /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 c3726c967..e0c8ff745 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 924e5f0c6..7a9e57708 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 000000000..62e793a27 --- /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 000000000..83ed98584 --- /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} + +