diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index cf66cac279..2c7b722a3f 100644 Binary files a/mobile/openapi/README.md and b/mobile/openapi/README.md differ diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index a870267f1a..b332e73e71 100644 Binary files a/mobile/openapi/lib/api.dart and b/mobile/openapi/lib/api.dart differ diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 0191f00059..f423676c5f 100644 Binary files a/mobile/openapi/lib/api_client.dart and b/mobile/openapi/lib/api_client.dart differ diff --git a/mobile/openapi/lib/model/purchase_response.dart b/mobile/openapi/lib/model/purchase_response.dart new file mode 100644 index 0000000000..284d899528 Binary files /dev/null and b/mobile/openapi/lib/model/purchase_response.dart differ diff --git a/mobile/openapi/lib/model/purchase_update.dart b/mobile/openapi/lib/model/purchase_update.dart new file mode 100644 index 0000000000..ca0a27e3bc Binary files /dev/null and b/mobile/openapi/lib/model/purchase_update.dart differ diff --git a/mobile/openapi/lib/model/user_preferences_response_dto.dart b/mobile/openapi/lib/model/user_preferences_response_dto.dart index 63fdfd49a7..21b96bb557 100644 Binary files a/mobile/openapi/lib/model/user_preferences_response_dto.dart and b/mobile/openapi/lib/model/user_preferences_response_dto.dart differ diff --git a/mobile/openapi/lib/model/user_preferences_update_dto.dart b/mobile/openapi/lib/model/user_preferences_update_dto.dart index ed1a779894..616883a60a 100644 Binary files a/mobile/openapi/lib/model/user_preferences_update_dto.dart and b/mobile/openapi/lib/model/user_preferences_update_dto.dart differ diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index da5b1e2ff3..731c3778c1 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -9775,6 +9775,32 @@ ], "type": "object" }, + "PurchaseResponse": { + "properties": { + "hideBuyButtonUntil": { + "type": "string" + }, + "showSupportBadge": { + "type": "boolean" + } + }, + "required": [ + "hideBuyButtonUntil", + "showSupportBadge" + ], + "type": "object" + }, + "PurchaseUpdate": { + "properties": { + "hideBuyButtonUntil": { + "type": "string" + }, + "showSupportBadge": { + "type": "boolean" + } + }, + "type": "object" + }, "QueueStatusDto": { "properties": { "isActive": { @@ -11742,13 +11768,17 @@ }, "memories": { "$ref": "#/components/schemas/MemoryResponse" + }, + "purchase": { + "$ref": "#/components/schemas/PurchaseResponse" } }, "required": [ "avatar", "download", "emailNotifications", - "memories" + "memories", + "purchase" ], "type": "object" }, @@ -11765,6 +11795,9 @@ }, "memories": { "$ref": "#/components/schemas/MemoryUpdate" + }, + "purchase": { + "$ref": "#/components/schemas/PurchaseUpdate" } }, "type": "object" diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index f2b03dcac1..93fe7f0c4c 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -95,11 +95,16 @@ export type EmailNotificationsResponse = { export type MemoryResponse = { enabled: boolean; }; +export type PurchaseResponse = { + hideBuyButtonUntil: string; + showSupportBadge: boolean; +}; export type UserPreferencesResponseDto = { avatar: AvatarResponse; download: DownloadResponse; emailNotifications: EmailNotificationsResponse; memories: MemoryResponse; + purchase: PurchaseResponse; }; export type AvatarUpdate = { color?: UserAvatarColor; @@ -115,11 +120,16 @@ export type EmailNotificationsUpdate = { export type MemoryUpdate = { enabled?: boolean; }; +export type PurchaseUpdate = { + hideBuyButtonUntil?: string; + showSupportBadge?: boolean; +}; export type UserPreferencesUpdateDto = { avatar?: AvatarUpdate; download?: DownloadUpdate; emailNotifications?: EmailNotificationsUpdate; memories?: MemoryUpdate; + purchase?: PurchaseUpdate; }; export type AlbumUserResponseDto = { role: AlbumUserRole; diff --git a/server/src/dtos/user-preferences.dto.ts b/server/src/dtos/user-preferences.dto.ts index 009908bb52..29cefcc10c 100644 --- a/server/src/dtos/user-preferences.dto.ts +++ b/server/src/dtos/user-preferences.dto.ts @@ -1,6 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; -import { IsEnum, IsInt, IsPositive, ValidateNested } from 'class-validator'; +import { IsDateString, IsEnum, IsInt, IsPositive, ValidateNested } from 'class-validator'; import { UserAvatarColor, UserPreferences } from 'src/entities/user-metadata.entity'; import { Optional, ValidateBoolean } from 'src/validation'; @@ -35,6 +35,15 @@ class DownloadUpdate { archiveSize?: number; } +class PurchaseUpdate { + @ValidateBoolean({ optional: true }) + showSupportBadge?: boolean; + + @IsDateString() + @Optional() + hideBuyButtonUntil?: string; +} + export class UserPreferencesUpdateDto { @Optional() @ValidateNested() @@ -55,6 +64,11 @@ export class UserPreferencesUpdateDto { @ValidateNested() @Type(() => DownloadUpdate) download?: DownloadUpdate; + + @Optional() + @ValidateNested() + @Type(() => PurchaseUpdate) + purchase?: PurchaseUpdate; } class AvatarResponse { @@ -77,11 +91,17 @@ class DownloadResponse { archiveSize!: number; } +class PurchaseResponse { + showSupportBadge!: boolean; + hideBuyButtonUntil!: string; +} + export class UserPreferencesResponseDto implements UserPreferences { memories!: MemoryResponse; avatar!: AvatarResponse; emailNotifications!: EmailNotificationsResponse; download!: DownloadResponse; + purchase!: PurchaseResponse; } export const mapPreferences = (preferences: UserPreferences): UserPreferencesResponseDto => { diff --git a/server/src/entities/user-metadata.entity.ts b/server/src/entities/user-metadata.entity.ts index 37384a6ba9..cbc889a5b9 100644 --- a/server/src/entities/user-metadata.entity.ts +++ b/server/src/entities/user-metadata.entity.ts @@ -45,6 +45,10 @@ export interface UserPreferences { download: { archiveSize: number; }; + purchase: { + showSupportBadge: boolean; + hideBuyButtonUntil: string; + }; } export const getDefaultPreferences = (user: { email: string }): UserPreferences => { @@ -68,6 +72,10 @@ export const getDefaultPreferences = (user: { email: string }): UserPreferences download: { archiveSize: HumanReadableSize.GiB * 4, }, + purchase: { + showSupportBadge: true, + hideBuyButtonUntil: new Date(2022, 1, 12).toISOString(), + }, }; }; diff --git a/server/src/utils/misc.spec.ts b/server/src/utils/misc.spec.ts index c36772ad43..53be77dc21 100644 --- a/server/src/utils/misc.spec.ts +++ b/server/src/utils/misc.spec.ts @@ -12,8 +12,9 @@ describe('getKeysDeep', () => { foo: 'bar', flag: true, count: 42, + date: new Date(), }), - ).toEqual(['foo', 'flag', 'count']); + ).toEqual(['foo', 'flag', 'count', 'date']); }); it('should skip undefined properties', () => { diff --git a/server/src/utils/misc.ts b/server/src/utils/misc.ts index e0a2ed860e..6063b4925c 100644 --- a/server/src/utils/misc.ts +++ b/server/src/utils/misc.ts @@ -33,7 +33,7 @@ export const getKeysDeep = (target: unknown, path: string[] = []) => { continue; } - if (_.isObject(value) && !_.isArray(value)) { + if (_.isObject(value) && !_.isArray(value) && !_.isDate(value)) { properties.push(...getKeysDeep(value, [...path, key])); continue; } diff --git a/web/src/app.css b/web/src/app.css index de9c9441cf..28ab712684 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -142,4 +142,46 @@ input:focus-visible { .scrollbar-stable { scrollbar-gutter: stable both-edges; } + + /* Supporter Effect */ + .supporter-effect { + position: relative; + border: 0px solid transparent; + background-clip: padding-box; + animation: gradient 10s ease infinite; + z-index: 1; + } + + .supporter-effect:hover:after { + position: absolute; + top: 0px; + bottom: 0px; + left: 0px; + right: 0px; + background: linear-gradient( + to right, + rgba(16, 132, 254, 0.25), + rgba(229, 125, 175, 0.25), + rgba(254, 36, 29, 0.25), + rgba(255, 183, 0, 0.25), + rgba(22, 193, 68, 0.25) + ); + content: ''; + border-radius: 8px; + animation: gradient 10s ease infinite; + background-size: 400% 400%; + z-index: -1; + } + + @keyframes gradient { + 0% { + background-position: 0% 50%; + } + 50% { + background-position: 100% 50%; + } + 100% { + background-position: 0% 50%; + } + } } diff --git a/web/src/lib/components/elements/buttons/button.svelte b/web/src/lib/components/elements/buttons/button.svelte index 76f52d7735..ce90a8f00f 100644 --- a/web/src/lib/components/elements/buttons/button.svelte +++ b/web/src/lib/components/elements/buttons/button.svelte @@ -2,6 +2,7 @@ export type Type = 'button' | 'submit' | 'reset'; export type Color = | 'primary' + | 'primary-inversed' | 'secondary' | 'transparent-primary' | 'text-primary' @@ -50,6 +51,8 @@ 'dark-gray': 'dark:border-immich-dark-gray dark:bg-gray-500 enabled:dark:hover:bg-immich-dark-primary/50 enabled:hover:bg-immich-primary/10 dark:text-white', 'overlay-primary': 'text-gray-500 enabled:hover:bg-gray-100', + 'primary-inversed': + 'bg-immich-dark-primary dark:bg-immich-primary text-black dark:text-white enabled:hover:bg-immich-dark-primary/80 enabled:dark:hover:bg-immich-primary/90', }; const sizeClasses: Record = { 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 deleted file mode 100644 index f77e854aec..0000000000 --- a/web/src/lib/components/shared-components/license/license-activation-success.svelte +++ /dev/null @@ -1,18 +0,0 @@ - - -
- -

{$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 deleted file mode 100644 index e5f780265d..0000000000 --- a/web/src/lib/components/shared-components/license/license-content.svelte +++ /dev/null @@ -1,70 +0,0 @@ - - -
-
-

- {$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 deleted file mode 100644 index 9f7e23c5d1..0000000000 --- a/web/src/lib/components/shared-components/license/license-modal.svelte +++ /dev/null @@ -1,25 +0,0 @@ - - - - - {#if showLicenseActivated} - - {:else} - { - showLicenseActivated = true; - }} - /> - {/if} - - diff --git a/web/src/lib/components/shared-components/license/user-license-card.svelte b/web/src/lib/components/shared-components/purchasing/individual-purchase-option-card.svelte similarity index 55% rename from web/src/lib/components/shared-components/license/user-license-card.svelte rename to web/src/lib/components/shared-components/purchasing/individual-purchase-option-card.svelte index 96f30c6857..64c9a81c05 100644 --- a/web/src/lib/components/shared-components/license/user-license-card.svelte +++ b/web/src/lib/components/shared-components/purchasing/individual-purchase-option-card.svelte @@ -1,39 +1,44 @@ - +
-

{$t('license_individual_title')}

+

{$t('purchase_individual_title')}

-

$24.99

-

{$t('license_per_user')}

+

$25

+

{$t('purchase_per_user')}

-

{$t('license_individual_description_1')}

+

{$t('purchase_individual_description_1')}

-

{$t('license_lifetime_description')}

+

{$t('purchase_lifetime_description')}

+
+ +
+ +

{$t('purchase_individual_description_2')}

- - + +
diff --git a/web/src/lib/components/shared-components/purchasing/purchase-activation-success.svelte b/web/src/lib/components/shared-components/purchasing/purchase-activation-success.svelte new file mode 100644 index 0000000000..df766aa3ae --- /dev/null +++ b/web/src/lib/components/shared-components/purchasing/purchase-activation-success.svelte @@ -0,0 +1,30 @@ + + +
+ +

{$t('purchase_activated_title')}

+

{$t('purchase_activated_subtitle')}

+ +
+ setSupportBadgeVisibility(detail)} + /> +
+ +
+ +
+
diff --git a/web/src/lib/components/shared-components/purchasing/purchase-content.svelte b/web/src/lib/components/shared-components/purchasing/purchase-content.svelte new file mode 100644 index 0000000000..8a01834409 --- /dev/null +++ b/web/src/lib/components/shared-components/purchasing/purchase-content.svelte @@ -0,0 +1,84 @@ + + +
+
+ {#if showTitle} +

+ {$t('purchase_option_title')} +

+ {/if} + + {#if showMessage} +
+

+ {$t('purchase_panel_info_1')} +

+
+

+ {$t('purchase_panel_info_2')} +

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

{$t('purchase_input_suggestion')}

+
+ + +
+
+
+
diff --git a/web/src/lib/components/shared-components/purchasing/purchase-modal.svelte b/web/src/lib/components/shared-components/purchasing/purchase-modal.svelte new file mode 100644 index 0000000000..52757bc32a --- /dev/null +++ b/web/src/lib/components/shared-components/purchasing/purchase-modal.svelte @@ -0,0 +1,26 @@ + + + + + {#if showProductActivated} + + {:else} + { + showProductActivated = true; + }} + showMessage={false} + /> + {/if} + + diff --git a/web/src/lib/components/shared-components/license/server-license-card.svelte b/web/src/lib/components/shared-components/purchasing/server-purchase-option-card.svelte similarity index 66% rename from web/src/lib/components/shared-components/license/server-license-card.svelte rename to web/src/lib/components/shared-components/purchasing/server-purchase-option-card.svelte index bfdbb3a665..4a650cefc6 100644 --- a/web/src/lib/components/shared-components/license/server-license-card.svelte +++ b/web/src/lib/components/shared-components/purchasing/server-purchase-option-card.svelte @@ -1,44 +1,44 @@ - +
-

{$t('license_server_title')}

+

{$t('purchase_server_title')}

-

$99.99

-

{$t('license_per_server')}

+

$100

+

{$t('purchase_per_server')}

-

{$t('license_server_description_1')}

+

{$t('purchase_server_description_1')}

-

{$t('license_lifetime_description')}

+

{$t('purchase_lifetime_description')}

-

{$t('license_server_description_2')}

+

{$t('purchase_server_description_2')}

- - + +
diff --git a/web/src/lib/components/shared-components/settings/setting-accordion.svelte b/web/src/lib/components/shared-components/settings/setting-accordion.svelte index 8d883019cb..3a367624a0 100755 --- a/web/src/lib/components/shared-components/settings/setting-accordion.svelte +++ b/web/src/lib/components/shared-components/settings/setting-accordion.svelte @@ -10,11 +10,20 @@ export let key: string; export let isOpen = $accordionState.has(key); + let accordionElement: HTMLDivElement; + $: setIsOpen(isOpen); const setIsOpen = (isOpen: boolean) => { if (isOpen) { $accordionState = $accordionState.add(key); + + setTimeout(() => { + accordionElement.scrollIntoView({ + behavior: 'smooth', + block: 'start', + }); + }, 200); } else { $accordionState.delete(key); $accordionState = $accordionState; @@ -26,7 +35,7 @@ }); -
+
-
- -
+ -
+
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 deleted file mode 100644 index eaa099b2a4..0000000000 --- a/web/src/lib/components/shared-components/side-bar/license-info.svelte +++ /dev/null @@ -1,117 +0,0 @@ - - -{#if isOpen} - (isOpen = false)} /> -{/if} - - - - - {#if showMessage && getAccountAge() > 14} -
(hoverMessage = true)} - on:mouseleave={() => (hoverMessage = false)} - on:focus={() => (hoverMessage = true)} - on:blur={() => (hoverMessage = false)} - role="dialog" - > -
- - { - showMessage = false; - }} - title={$t('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/purchase-info.svelte b/web/src/lib/components/shared-components/side-bar/purchase-info.svelte new file mode 100644 index 0000000000..da959266c1 --- /dev/null +++ b/web/src/lib/components/shared-components/side-bar/purchase-info.svelte @@ -0,0 +1,179 @@ + + +{#if isOpen} + (isOpen = false)} /> +{/if} + +{#if getAccountAge() > 14} + +{/if} + + + {#if showMessage} +
(hoverMessage = true)} + on:mouseleave={() => (hoverMessage = false)} + on:focus={() => (hoverMessage = true)} + on:blur={() => (hoverMessage = false)} + role="dialog" + > +
+
+ +
+ { + showMessage = false; + }} + title={$t('close')} + size="18" + class="text-immich-dark-gray/85 dark:text-immich-gray" + /> +
+ +

+ {$t('purchase_panel_title')} +

+ +
+

+ {$t('purchase_panel_info_1')} +

+
+

+ {$t('purchase_panel_info_2')} +

+
+ + +
+ + +
+
+ {/if} +
diff --git a/web/src/lib/components/user-settings-page/license-settings.svelte b/web/src/lib/components/user-settings-page/license-settings.svelte deleted file mode 100644 index a88a89486f..0000000000 --- a/web/src/lib/components/user-settings-page/license-settings.svelte +++ /dev/null @@ -1,172 +0,0 @@ - - -
-
- {#if $isLicenseActivated} - {#if isServerLicense} -
- - -
-

Server License

- - {#if $user.isAdmin && serverLicenseInfo?.activatedAt} -

- Activated on {new Date(serverLicenseInfo?.activatedAt).toLocaleDateString()} -

- {:else} -

Your license is managed by the admin

- {/if} -
-
- - {#if $user.isAdmin} -
- -
- {/if} - {:else} -
- - -
-

Individual License

- {#if $user.license?.activatedAt} -

- Activated on {new Date($user.license?.activatedAt).toLocaleDateString()} -

- {/if} -
-
- -
- -
- {/if} - {:else} - {#if accountAge > 14} -
-

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

-
- {/if} - - {/if} -
-
diff --git a/web/src/lib/components/user-settings-page/user-purchase-settings.svelte b/web/src/lib/components/user-settings-page/user-purchase-settings.svelte new file mode 100644 index 0000000000..8af38fa905 --- /dev/null +++ b/web/src/lib/components/user-settings-page/user-purchase-settings.svelte @@ -0,0 +1,180 @@ + + +
+
+ {#if $isPurchased} + +
+ setSupportBadgeVisibility(detail)} + /> +
+ + + {#if isServerProduct} +
+ + +
+

+ {$t('purchase_server_title')} +

+ + {#if $user.isAdmin && serverPurchaseInfo?.activatedAt} +

+ {$t('purchase_activated_time', { + values: { date: new Date(serverPurchaseInfo.activatedAt).toLocaleDateString() }, + })} +

+ {:else} +

{$t('purchase_settings_server_activated')}

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

+ {$t('purchase_individual_title')} +

+ {#if $user.license?.activatedAt} +

+ {$t('purchase_activated_time', { + values: { date: new Date($user.license?.activatedAt).toLocaleDateString() }, + })} +

+ {/if} +
+
+ +
+ +
+ {/if} + {:else} + + {/if} +
+
diff --git a/web/src/lib/components/user-settings-page/user-settings-list.svelte b/web/src/lib/components/user-settings-page/user-settings-list.svelte index db81273377..df32126a2d 100644 --- a/web/src/lib/components/user-settings-page/user-settings-list.svelte +++ b/web/src/lib/components/user-settings-page/user-settings-list.svelte @@ -18,7 +18,7 @@ import NotificationsSettings from '$lib/components/user-settings-page/notifications-settings.svelte'; import { t } from 'svelte-i18n'; import DownloadSettings from '$lib/components/user-settings-page/download-settings.svelte'; - import LicenseSettings from '$lib/components/user-settings-page/license-settings.svelte'; + import UserPurchaseSettings from '$lib/components/user-settings-page/user-purchase-settings.svelte'; export let keys: ApiKeyResponseDto[] = []; export let sessions: SessionResponseDto[] = []; @@ -53,14 +53,6 @@ - - - - @@ -87,4 +79,12 @@ + + + + diff --git a/web/src/lib/constants.ts b/web/src/lib/constants.ts index 7e82ef75bc..a5f92964a6 100644 --- a/web/src/lib/constants.ts +++ b/web/src/lib/constants.ts @@ -311,7 +311,7 @@ export const langs = [ { name: 'Development (keys only)', code: 'dev', loader: () => Promise.resolve({}) }, ]; -export enum ImmichLicense { +export enum ImmichProduct { Client = 'immich-client', Server = 'immich-server', } diff --git a/web/src/lib/i18n/en.json b/web/src/lib/i18n/en.json index 24048716c9..0ac69f3fe4 100644 --- a/web/src/lib/i18n/en.json +++ b/web/src/lib/i18n/en.json @@ -405,7 +405,7 @@ "bulk_delete_duplicates_confirmation": "Are you sure you want to bulk delete {count, plural, one {# duplicate asset} other {# duplicate assets}}? This will keep the largest asset of each group and permanently delete all other duplicates. You cannot undo this action!", "bulk_keep_duplicates_confirmation": "Are you sure you want to keep {count, plural, one {# duplicate asset} other {# duplicate assets}}? This will resolve all duplicate groups without deleting anything.", "bulk_trash_duplicates_confirmation": "Are you sure you want to bulk trash {count, plural, one {# duplicate asset} other {# duplicate assets}}? This will keep the largest asset of each group and trash all other duplicates.", - "buy": "Purchase License", + "buy": "Purchase Immich", "camera": "Camera", "camera_brand": "Camera brand", "camera_model": "Camera model", @@ -747,31 +747,6 @@ "level": "Level", "library": "Library", "library_options": "Library options", - "license_account_info": "Your account is licensed", - "license_activated_subtitle": "Thank you for supporting Immich and open-source software", - "license_activated_title": "Your license has been successfully activated", - "license_button_activate": "Activate", - "license_button_buy": "Buy", - "license_button_buy_license": "Buy License", - "license_button_select": "Select", - "license_failed_activation": "Failed to activate license. Please check your email for the correct license key!", - "license_individual_description_1": "1 license per user on any server", - "license_individual_title": "Individual License", - "license_info_licensed": "Licensed", - "license_info_unlicensed": "Unlicensed", - "license_input_suggestion": "Have a license? Enter the key below", - "license_license_subtitle": "Buy a license to support Immich", - "license_license_title": "LICENSE", - "license_lifetime_description": "Lifetime license", - "license_per_server": "Per server", - "license_per_user": "Per user", - "license_server_description_1": "1 license per server", - "license_server_description_2": "License for all users on the server", - "license_server_title": "Server License", - "license_trial_info_1": "You are running an Unlicensed version of Immich", - "license_trial_info_2": "You have been using Immich for approximately", - "license_trial_info_3": "{accountAge, plural, one {# day} other {# days}}", - "license_trial_info_4": "Please consider purchasing a license to support the continued development of the service", "light": "Light", "like_deleted": "Like deleted", "link_options": "Link options", @@ -939,6 +914,34 @@ "profile_picture_set": "Profile picture set.", "public_album": "Public album", "public_share": "Public Share", + "purchase_account_info": "Supporter", + "purchase_activated_subtitle": "Thank you for supporting Immich and open-source software", + "purchase_activated_time": "Activated on {date}", + "purchase_activated_title": "Your key has been successfully activated", + "purchase_button_activate": "Activate", + "purchase_button_buy": "Buy", + "purchase_button_buy_immich": "Buy Immich", + "purchase_button_never_show_again": "Never show again", + "purchase_button_reminder": "Remind me in 30 days", + "purchase_button_remove_key": "Remove key", + "purchase_button_select": "Select", + "purchase_failed_activation": "Failed to activate! Please check your email for the the correct product key!", + "purchase_individual_description_1": "For an individual", + "purchase_individual_description_2": "Supporter status", + "purchase_individual_title": "Individual", + "purchase_input_suggestion": "Have a product key? Enter the key below", + "purchase_license_subtitle": "Buy Immich to support the continued development of the service", + "purchase_lifetime_description": "Lifetime purchase", + "purchase_option_title": "PURCHASE OPTIONS", + "purchase_panel_info_1": "Building Immich takes a lot of time and effort, and we have full-time engineers working on it to make it as good as we possibly can. Our mission is for open-source software and ethical business practices to become a sustainable income source for developers and to create a privacy-respecting ecosystem with real alternatives to exploitative cloud services.", + "purchase_panel_info_2": "As we’re committed not to add paywalls, this purchase will not grant you any additional features in Immich. We rely on users like you to support Immich’s ongoing development.", + "purchase_panel_title": "Support the project", + "purchase_per_server": "Per server", + "purchase_per_user": "Per user", + "purchase_server_description_1": "For the whole server", + "purchase_server_description_2": "Supporter status", + "purchase_server_title": "Server", + "purchase_settings_server_activated": "The server product key is managed by the admin", "reaction_options": "Reaction options", "read_changelog": "Read Changelog", "reassign": "Reassign", @@ -1078,6 +1081,8 @@ "show_person_options": "Show person options", "show_progress_bar": "Show Progress Bar", "show_search_options": "Show search options", + "show_supporter_badge": "Supporter badge", + "show_supporter_badge_description": "Show a supporter badge", "shuffle": "Shuffle", "sign_out": "Sign Out", "sign_up": "Sign up", @@ -1168,9 +1173,9 @@ "use_custom_date_range": "Use custom date range instead", "user": "User", "user_id": "User ID", - "user_license_settings": "License", - "user_license_settings_description": "Manage your license", "user_liked": "{user} liked {type, select, photo {this photo} video {this video} asset {this asset} other {it}}", + "user_purchase_settings": "Purchase", + "user_purchase_settings_description": "Manage your purchase", "user_role_set": "Set {user} as {role}", "user_usage_detail": "User usage detail", "username": "Username", diff --git a/web/src/lib/stores/license.store.ts b/web/src/lib/stores/license.store.ts deleted file mode 100644 index aecfae31bb..0000000000 --- a/web/src/lib/stores/license.store.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { writable } from 'svelte/store'; - -function createLicenseStore() { - const isLicenseActivated = writable(false); - - function setLicenseStatus(status: boolean) { - isLicenseActivated.set(status); - } - - return { - isLicenseActivated: { - subscribe: isLicenseActivated.subscribe, - }, - setLicenseStatus, - }; -} - -export const licenseStore = createLicenseStore(); diff --git a/web/src/lib/stores/purchase.store.ts b/web/src/lib/stores/purchase.store.ts new file mode 100644 index 0000000000..e21a4b804b --- /dev/null +++ b/web/src/lib/stores/purchase.store.ts @@ -0,0 +1,18 @@ +import { writable } from 'svelte/store'; + +function createPurchaseStore() { + const isPurcharsed = writable(false); + + function setPurchaseStatus(status: boolean) { + isPurcharsed.set(status); + } + + return { + isPurchased: { + subscribe: isPurcharsed.subscribe, + }, + setPurchaseStatus, + }; +} + +export const purchaseStore = createPurchaseStore(); diff --git a/web/src/lib/stores/user.store.ts b/web/src/lib/stores/user.store.ts index 920ec4047f..5bffc08b80 100644 --- a/web/src/lib/stores/user.store.ts +++ b/web/src/lib/stores/user.store.ts @@ -1,4 +1,4 @@ -import { licenseStore } from '$lib/stores/license.store'; +import { purchaseStore } from '$lib/stores/purchase.store'; import { type UserAdminResponseDto, type UserPreferencesResponseDto } from '@immich/sdk'; import { writable } from 'svelte/store'; @@ -12,5 +12,5 @@ export const preferences = writable(); export const resetSavedUser = () => { user.set(undefined as unknown as UserAdminResponseDto); preferences.set(undefined as unknown as UserPreferencesResponseDto); - licenseStore.setLicenseStatus(false); + purchaseStore.setPurchaseStatus(false); }; diff --git a/web/src/lib/utils/auth.ts b/web/src/lib/utils/auth.ts index 78b613299b..d37f1bb960 100644 --- a/web/src/lib/utils/auth.ts +++ b/web/src/lib/utils/auth.ts @@ -1,5 +1,5 @@ import { browser } from '$app/environment'; -import { licenseStore } from '$lib/stores/license.store'; +import { purchaseStore } from '$lib/stores/purchase.store'; import { serverInfo } from '$lib/stores/server-info.store'; import { preferences as preferences$, user as user$ } from '$lib/stores/user.store'; import { getAboutInfo, getMyPreferences, getMyUser, getStorage } from '@immich/sdk'; @@ -26,7 +26,7 @@ export const loadUser = async () => { // Check for license status if (serverInfo.licensed || user.license?.activatedAt) { - licenseStore.setLicenseStatus(true); + purchaseStore.setPurchaseStatus(true); } } return user; diff --git a/web/src/lib/utils/license-utils.ts b/web/src/lib/utils/license-utils.ts index 077476d75c..6b429a0115 100644 --- a/web/src/lib/utils/license-utils.ts +++ b/web/src/lib/utils/license-utils.ts @@ -1,11 +1,11 @@ import { PUBLIC_IMMICH_BUY_HOST, PUBLIC_IMMICH_PAY_HOST } from '$env/static/public'; -import type { ImmichLicense } from '$lib/constants'; +import type { ImmichProduct } from '$lib/constants'; import { serverConfig } from '$lib/stores/server-config.store'; import { setServerLicense, setUserLicense, type LicenseResponseDto } from '@immich/sdk'; import { get } from 'svelte/store'; import { loadUser } from './auth'; -export const activateLicense = async (licenseKey: string, activationKey: string): Promise => { +export const activateProduct = async (licenseKey: string, activationKey: string): Promise => { // Send server key to user activation if user is not admin const user = await loadUser(); const isServerActivation = user?.isAdmin && licenseKey.search('IMSV') !== -1; @@ -21,7 +21,7 @@ export const getActivationKey = async (licenseKey: string): Promise => { return response.text(); }; -export const getLicenseLink = (license: ImmichLicense) => { +export const getLicenseLink = (license: ImmichProduct) => { const url = new URL('/', PUBLIC_IMMICH_BUY_HOST); url.searchParams.append('productId', license); url.searchParams.append('instanceUrl', get(serverConfig).externalDomain || window.origin); diff --git a/web/src/lib/utils/purchase-utils.ts b/web/src/lib/utils/purchase-utils.ts new file mode 100644 index 0000000000..7cf08e866c --- /dev/null +++ b/web/src/lib/utils/purchase-utils.ts @@ -0,0 +1,32 @@ +import { preferences } from '$lib/stores/user.store'; +import { updateMyPreferences } from '@immich/sdk'; +import { DateTime } from 'luxon'; +import { get } from 'svelte/store'; + +export const getButtonVisibility = (): boolean => { + const myPreferences = get(preferences); + + if (!myPreferences) { + return true; + } + + const { purchase } = myPreferences; + + const now = DateTime.now(); + const hideUntilDate = DateTime.fromISO(purchase.hideBuyButtonUntil); + const dayLeft = Number(now.diff(hideUntilDate, 'days').days.toFixed(0)); + + return dayLeft > 0; +}; + +export const setSupportBadgeVisibility = async (value: boolean) => { + const response = await updateMyPreferences({ + userPreferencesUpdateDto: { + purchase: { + showSupportBadge: value, + }, + }, + }); + + preferences.set(response); +}; diff --git a/web/src/routes/(user)/buy/+page.svelte b/web/src/routes/(user)/buy/+page.svelte index 4f0b0644c2..23e7c4aea9 100644 --- a/web/src/routes/(user)/buy/+page.svelte +++ b/web/src/routes/(user)/buy/+page.svelte @@ -1,41 +1,42 @@
-
+
{#if data.isActivated === false} {/if} - {#if $isLicenseActivated} + {#if $isPurchased} {/if} diff --git a/web/src/routes/(user)/buy/+page.ts b/web/src/routes/(user)/buy/+page.ts index 9c34573d5d..ba55948b1e 100644 --- a/web/src/routes/(user)/buy/+page.ts +++ b/web/src/routes/(user)/buy/+page.ts @@ -1,7 +1,7 @@ -import { licenseStore } from '$lib/stores/license.store'; +import { purchaseStore } from '$lib/stores/purchase.store'; import { authenticate } from '$lib/utils/auth'; import { getFormatter } from '$lib/utils/i18n'; -import { activateLicense, getActivationKey } from '$lib/utils/license-utils'; +import { activateProduct, getActivationKey } from '$lib/utils/license-utils'; import type { PageLoad } from './$types'; export const load = (async ({ url }) => { @@ -18,10 +18,10 @@ export const load = (async ({ url }) => { } if (licenseKey && activationKey) { - const response = await activateLicense(licenseKey, activationKey); + const response = await activateProduct(licenseKey, activationKey); if (response.activatedAt !== '') { isActivated = true; - licenseStore.setLicenseStatus(true); + purchaseStore.setPurchaseStatus(true); } } } catch (error) {