From ef7a6bb24616fe2ed852faecff243386bc8f0d2b Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 26 Jul 2024 10:34:35 -0500 Subject: [PATCH] chore(web): change license wording and other things (#11309) --- mobile/openapi/README.md | Bin 30982 -> 31074 bytes mobile/openapi/lib/api.dart | Bin 10511 -> 10583 bytes mobile/openapi/lib/api_client.dart | Bin 27540 -> 27704 bytes .../openapi/lib/model/purchase_response.dart | Bin 0 -> 3272 bytes mobile/openapi/lib/model/purchase_update.dart | Bin 0 -> 4140 bytes .../model/user_preferences_response_dto.dart | Bin 3894 -> 4154 bytes .../model/user_preferences_update_dto.dart | Bin 5594 -> 6284 bytes open-api/immich-openapi-specs.json | 35 +++- open-api/typescript-sdk/src/fetch-client.ts | 10 + server/src/dtos/user-preferences.dto.ts | 22 ++- server/src/entities/user-metadata.entity.ts | 8 + server/src/utils/misc.spec.ts | 3 +- server/src/utils/misc.ts | 2 +- web/src/app.css | 42 ++++ .../components/elements/buttons/button.svelte | 3 + .../license/license-activation-success.svelte | 18 -- .../license/license-content.svelte | 70 ------- .../license/license-modal.svelte | 25 --- .../individual-purchase-option-card.svelte} | 25 ++- .../purchase-activation-success.svelte | 30 +++ .../purchasing/purchase-content.svelte | 84 ++++++++ .../purchasing/purchase-modal.svelte | 26 +++ .../server-purchase-option-card.svelte} | 20 +- .../settings/setting-accordion.svelte | 11 +- .../side-bar/bottom-info.svelte | 8 +- .../side-bar/license-info.svelte | 117 ------------ .../side-bar/purchase-info.svelte | 179 +++++++++++++++++ .../license-settings.svelte | 172 ----------------- .../user-purchase-settings.svelte | 180 ++++++++++++++++++ .../user-settings-list.svelte | 18 +- web/src/lib/constants.ts | 2 +- web/src/lib/i18n/en.json | 61 +++--- web/src/lib/stores/license.store.ts | 18 -- web/src/lib/stores/purchase.store.ts | 18 ++ web/src/lib/stores/user.store.ts | 4 +- web/src/lib/utils/auth.ts | 4 +- web/src/lib/utils/license-utils.ts | 6 +- web/src/lib/utils/purchase-utils.ts | 32 ++++ web/src/routes/(user)/buy/+page.svelte | 27 +-- web/src/routes/(user)/buy/+page.ts | 8 +- 40 files changed, 776 insertions(+), 512 deletions(-) create mode 100644 mobile/openapi/lib/model/purchase_response.dart create mode 100644 mobile/openapi/lib/model/purchase_update.dart delete mode 100644 web/src/lib/components/shared-components/license/license-activation-success.svelte delete mode 100644 web/src/lib/components/shared-components/license/license-content.svelte delete mode 100644 web/src/lib/components/shared-components/license/license-modal.svelte rename web/src/lib/components/shared-components/{license/user-license-card.svelte => purchasing/individual-purchase-option-card.svelte} (55%) create mode 100644 web/src/lib/components/shared-components/purchasing/purchase-activation-success.svelte create mode 100644 web/src/lib/components/shared-components/purchasing/purchase-content.svelte create mode 100644 web/src/lib/components/shared-components/purchasing/purchase-modal.svelte rename web/src/lib/components/shared-components/{license/server-license-card.svelte => purchasing/server-purchase-option-card.svelte} (66%) delete mode 100644 web/src/lib/components/shared-components/side-bar/license-info.svelte create mode 100644 web/src/lib/components/shared-components/side-bar/purchase-info.svelte delete mode 100644 web/src/lib/components/user-settings-page/license-settings.svelte create mode 100644 web/src/lib/components/user-settings-page/user-purchase-settings.svelte delete mode 100644 web/src/lib/stores/license.store.ts create mode 100644 web/src/lib/stores/purchase.store.ts create mode 100644 web/src/lib/utils/purchase-utils.ts diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index cf66cac279df0ac01ec0f56668c83fbff34bc54b..2c7b722a3f6f690f87168b9c955f0c3018a5da2f 100644 GIT binary patch delta 98 zcmZqs#Q5kF;|BlK;DFMiR6iqG#U4>|vL})=u PVhOrxh~Q?o)C0l*f`uis delta 14 WcmaF#iLvby;|BlK&8t(l3j+W;;Riqf diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index a870267f1ad13f79f6955213bf7ea29bad2bda51..b332e73e71210b554a1f03b5281e42b87f7bc764 100644 GIT binary patch delta 38 lcmeAVx*oLQfCNWDX;E@UVsYwZ15sH7=QOYM<^vM`0st;@4!!^Y delta 12 TcmcZ})E~6rfW&4FNq+$VCI$q? diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 0191f00059026dc23ebe9c176160f027c6b860f4..f423676c5f2a9405f78f9dde871b177c34803f65 100644 GIT binary patch delta 66 wcmbPoopHwv#tqF*908?8$r*{osgoTwWe}W+1{z2lO(_Itqo&&CW+y>S09#!czyJUM delta 14 VcmdmSgK^4r#tqF*o7J5KH32id1FxXZ^b)QRQop7IUPj9)#AO)iz`^xBrFTx z{1iqBQ+hDK?F`5j$ed>^7XkjC4+c@nw1z(mg;tv1a-HLb93BP$BLv`n7sc+PFh*vdv82@RvC@g$-W55adcoo~C!75# z<_mCVl*rc5jbq>5OPNC3+rD`xzj38-cZ^}J5D3MLR4^WUv->YRLU>HV)hUc8;NXr} z>QUBy_2&C`_X|W1XfI#dN^FpxI&q(#tl`M3ojLim*E-dR%)pu(Kn9cFWXv&tn@BBS z(rv~gt~FCT(j7`E;aX^G=5?VAs!*r!%GtD&I9PPjDG4&t{qQWF!?rjU-Cf5ox@&%? z{PBx+F3X|K;hN=d-IhMYJM1wLO~A;HT4tDo)8a~GEQK$)1Y_t}KkUzKwO}n4YS_(Z zxJ1WCgxqp&r+IK~zj2Pr_L|+A7WO3e|I~xPvKZVb(jbh6Z!9gihLw_=GSWzL-P}-5uJ{TEik(HOh_GjLNZl0TXfh%Saf&4}CN;T*&LsHN z=3tp9nQ^&Qv|u`(&5ZgA6ROq$jtH|RLBzD_Q0KYf;N}g@tWFLdrsw{;te(M!<$n=c zv(V%H@GNfo6t@?8aRh&ZS&zF7M4K)_x7_?1aVUP zE80Nl8)>GS-0Z5D$REyEuM91>cwAW1!~2S1$Nsp(T0r8p)nMW!I0Vo^F(+dy-eyQT zbEu4f#j(>>!_z@qL3{*s>18C9T4(yhu~MTvVw#SkS6Di#lPd zq11#O_x&O2mY%U=<*|0b*;3wb&Y-@Egm@dkOAlRc+~IJrJi#dfwQ!FA@Ewg|>+dv4oBx1Vbgdf*%f?3GSyQ zhj)MNL5w9|Cc?PkZ-ZZ-_xNWzSK7$wOq=N}&Eydks<1Md%1kb$a#8g=UFO=johL-L z5^Fon3pX|L?;O`1}dlzP)*Ie>U`ug@BY+#|b~vS_<$7kYJ8&Y_cX122qQ6V4Y|LJRRj(v_47Lh7k1vP@iMa*xhZ6-$^Q z$>uT!da-b+R>Z+9l_y$SRt<0BtVkrBP2@}znWG;fDBWEowF_&YhxcC&8aTbfL6oqIeP{x^s{z%wM0 zB#7-W0CwNvpuYg;u?@%=Jq{EUyPzQ_Hg29XS6 zh7hqBCo9;b4Go9=@zpmOBZWa{b3nrpK@RB@i>%KPj;fuXKf^ad)SGEZPo6+Ah3y@Q ziCcCsqIs%qH=k**b$m&cqq%h8yIs8DR2YuDA)6A*l~u`DS}V+&DJI5 zZkuws?=AQ&^54US5hZJMku)B`hL2V&SP*LNEL9?-Z&;!sb)>IvUU)3h8F|v|BEeoj zwo*u5*~IL3P2a!79k&$uaL*0N45d4mhG^fTiz8YdY?|2ygl;pkEx0wYy>qvLZE3Hk zZJzP{(fh>OZpyax(7s7RB@{*nOR2M=;aHj(y@Wb$0TbVY&}wH8*ym0>0SPXnzk z@`T=q3IlVhbwXdQk+V)rTYP_`3NEm|8~<5)Q4z#5p~Uu63ILk!jVQz7JA}Cb za&X|<7yh}?E1qGHV3-)JjBYBF1I9*L9#N3uE!C6DU>6686>=9~6HvuHQ3t6#)$U}O zyLF`zaYKjcmx}d@M+!c=-LE1WqCRZ#Wx#~>p1|l57KUcalTNnsc5q>U;U=)Db~1ok zf!lz%JmoYy)vsVWqUxL1QpdkuG+S6PlXga-@;pESsd_DKDXEv#3UJ(cC*EFe7~*kT zf*}9530*xErkK|8DB;IPXpN27Vb!dw=neQ+DsANpLmvFOFN$8xz)RI)a!)h?(j*-G zF4x=_{Q1B|zWR=k;%KdWa!|O?d5mj*YH>k+UU8}d zOm_1ECJsg=1vND;1t7>T$w)1N>r}9{g{zyqf_bNaf;L!{22AthaF$gfI&d|%Dlm!7 zjI6AT$_ffu#rb*BMd~nN^;iX4RL5;%TgoJ&05`!(ApquTeGW+un7++3Ii|CTDJUQe O(g8Z8)|#u9iwgimb5x!H delta 46 zcmV+}0MY-tAhsT`7y^?t0*$lZ0s{e)1O&O0*94%mivvWB>pF diff --git a/mobile/openapi/lib/model/user_preferences_update_dto.dart b/mobile/openapi/lib/model/user_preferences_update_dto.dart index ed1a7798948801a29f8b934cd6d8ea93ee7157bd..616883a60a26449d1a868023164f7d9753ac5381 100644 GIT binary patch delta 297 zcmcbm-D9}n9V5SjLP;7Xpc zvoR_usHt%&06~69MrsjUyMnDPLfsn 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) {