Compare commits

..

1 Commits

Author SHA1 Message Date
Jason Rasmussen
2aab5c28a1 refactor: purchase store 2026-01-30 16:41:11 -05:00
27 changed files with 58 additions and 101 deletions

View File

@@ -1,33 +0,0 @@
# v2.5.3
## Highlights
{{RELEASE HIGHLIGHTS}}
As always, please consider supporting the project.
🎉 Cheers! 🎉
----
And as always, bugs are fixed, and many other improvements also come with this release.
<!-- Release notes generated using configuration in .github/release.yml at main -->
## What's Changed
### 🐛 Bug fixes
* chore: remove random code snippet by @jrasm91 in https://github.com/immich-app/immich/pull/25677
* fix: reset and unsaved change states in editor by @bwees in https://github.com/immich-app/immich/pull/25588
* fix: no notification if release check is disabled by @jrasm91 in https://github.com/immich-app/immich/pull/25688
* fix(mobile): hide latest version if disabled by @uhthomas in https://github.com/immich-app/immich/pull/25691
### 📚 Documentation
* docs(openapi): Add descriptions to OpenAPI specification by @timonrieger in https://github.com/immich-app/immich/pull/25185
* fix(docs): clarify supported vector version by @mmomjian in https://github.com/immich-app/immich/pull/25753
**Full Changelog**: https://github.com/immich-app/immich/compare/v2.5.2...v2.5.3
---

View File

@@ -1,6 +1,6 @@
{
"name": "@immich/cli",
"version": "2.5.3",
"version": "2.5.2",
"description": "Command Line Interface (CLI) for Immich",
"type": "module",
"exports": "./dist/index.js",

View File

@@ -88,7 +88,7 @@ The easiest option is to have both extensions installed during the migration:
<details>
<summary>Migration steps (automatic)</summary>
1. Ensure you still have pgvecto.rs installed
2. Install `pgvector` (`>= 0.7, < 0.9`). The easiest way to do this is on Debian/Ubuntu by adding the [PostgreSQL Apt repository][pg-apt] and then running `apt install postgresql-NN-pgvector`, where `NN` is your Postgres version (e.g., `16`)
2. Install `pgvector` (`>= 0.7.0, < 1.0.0`). The easiest way to do this is on Debian/Ubuntu by adding the [PostgreSQL Apt repository][pg-apt] and then running `apt install postgresql-NN-pgvector`, where `NN` is your Postgres version (e.g., `16`)
3. [Install VectorChord][vchord-install]
4. Add `shared_preload_libraries= 'vchord.so, vectors.so'` to your `postgresql.conf`, making sure to include _both_ `vchord.so` and `vectors.so`. You may include other libraries here as well if needed
5. Restart the Postgres database

View File

@@ -1,7 +1,7 @@
[
{
"label": "v2.5.3",
"url": "https://docs.v2.5.3.archive.immich.app"
"label": "v2.5.2",
"url": "https://docs.v2.5.2.archive.immich.app"
},
{
"label": "v2.4.1",

View File

@@ -1,6 +1,6 @@
{
"name": "immich-e2e",
"version": "2.5.3",
"version": "2.5.2",
"description": "",
"main": "index.js",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "immich-i18n",
"version": "2.5.3",
"version": "2.5.2",
"private": true,
"scripts": {
"format": "prettier --check .",

View File

@@ -1,6 +1,6 @@
[project]
name = "immich-ml"
version = "2.5.3"
version = "2.5.2"
description = ""
authors = [{ name = "Hau Tran", email = "alex.tran1502@gmail.com" }]
requires-python = ">=3.11,<4.0"

View File

@@ -919,7 +919,7 @@ wheels = [
[[package]]
name = "immich-ml"
version = "2.5.3"
version = "2.5.2"
source = { editable = "." }
dependencies = [
{ name = "aiocache" },

View File

@@ -35,8 +35,8 @@ platform :android do
task: 'bundle',
build_type: 'Release',
properties: {
"android.injected.version.code" => 3034,
"android.injected.version.name" => "2.5.3",
"android.injected.version.code" => 3033,
"android.injected.version.name" => "2.5.2",
}
)
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')

View File

@@ -80,7 +80,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>2.5.3</string>
<string>2.5.2</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>

View File

@@ -3,7 +3,7 @@ Immich API
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
- API version: 2.5.3
- API version: 2.5.2
- Generator version: 7.8.0
- Build package: org.openapitools.codegen.languages.DartClientCodegen

View File

@@ -2,7 +2,7 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone
publish_to: 'none'
version: 2.5.3+3034
version: 2.5.2+3033
environment:
sdk: '>=3.8.0 <4.0.0'

View File

@@ -15057,7 +15057,7 @@
"info": {
"title": "Immich",
"description": "Immich API",
"version": "2.5.3",
"version": "2.5.2",
"contact": {}
},
"tags": [

View File

@@ -1,6 +1,6 @@
{
"name": "@immich/sdk",
"version": "2.5.3",
"version": "2.5.2",
"description": "Auto-generated TypeScript SDK for the Immich API",
"type": "module",
"main": "./build/index.js",

View File

@@ -1,6 +1,6 @@
/**
* Immich
* 2.5.3
* 2.5.2
* DO NOT MODIFY - This file has been generated using oazapfts.
* See https://www.npmjs.com/package/oazapfts
*/

View File

@@ -1,6 +1,6 @@
{
"name": "immich-monorepo",
"version": "2.5.3",
"version": "2.5.2",
"description": "Monorepo for Immich",
"private": true,
"packageManager": "pnpm@10.28.0+sha512.05df71d1421f21399e053fde567cea34d446fa02c76571441bfc1c7956e98e363088982d940465fd34480d4d90a0668bc12362f8aa88000a64e83d0b0e47be48",

View File

@@ -1,6 +1,6 @@
{
"name": "immich",
"version": "2.5.3",
"version": "2.5.2",
"description": "",
"author": "",
"private": true,

View File

@@ -1,6 +1,6 @@
{
"name": "immich-web",
"version": "2.5.3",
"version": "2.5.2",
"license": "GNU Affero General Public License version 3",
"type": "module",
"scripts": {

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { purchaseStore } from '$lib/stores/purchase.store';
import { authManager } from '$lib/managers/auth-manager.svelte';
import { handleError } from '$lib/utils/handle-error';
import { activateProduct, getActivationKey } from '$lib/utils/license-utils';
import { Button, Heading, LoadingSpinner } from '@immich/ui';
@@ -26,7 +26,7 @@
await activateProduct(productKey, activationKey);
onActivate();
purchaseStore.setPurchaseStatus(true);
authManager.isPurchased = true;
} catch (error) {
handleError(error, $t('purchase_failed_activation'));
} finally {

View File

@@ -2,9 +2,9 @@
import { goto } from '$app/navigation';
import { OpenQueryParam } from '$lib/constants';
import Portal from '$lib/elements/Portal.svelte';
import { authManager } from '$lib/managers/auth-manager.svelte';
import PurchaseModal from '$lib/modals/PurchaseModal.svelte';
import { Route } from '$lib/route';
import { purchaseStore } from '$lib/stores/purchase.store';
import { preferences } from '$lib/stores/user.store';
import { getAccountAge } from '$lib/utils/auth';
import { handleError } from '$lib/utils/handle-error';
@@ -22,8 +22,6 @@
let showBuyButton = $state(getButtonVisibility());
const { isPurchased } = purchaseStore;
const openPurchaseModal = async () => {
await modalManager.show(PurchaseModal);
showMessage = false;
@@ -72,7 +70,7 @@
</script>
<div class="license-status ps-4 text-sm">
{#if $isPurchased && $preferences.purchase.showSupportBadge}
{#if authManager.isPurchased && $preferences.purchase.showSupportBadge}
<button
onclick={() => goto(Route.userSettings({ isOpen: OpenQueryParam.PURCHASE_SETTINGS }))}
class="w-full mt-2"
@@ -80,7 +78,7 @@
>
<SupporterBadge size="small" effect="always" />
</button>
{:else if !$isPurchased && showBuyButton && getAccountAge() > 14}
{:else if !authManager.isPurchased && showBuyButton && getAccountAge() > 14}
<button
type="button"
onclick={openPurchaseModal}

View File

@@ -4,8 +4,8 @@
import PurchaseContent from '$lib/components/shared-components/purchasing/purchase-content.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import { dateFormats } from '$lib/constants';
import { authManager } from '$lib/managers/auth-manager.svelte';
import { locale } from '$lib/stores/preferences.store';
import { purchaseStore } from '$lib/stores/purchase.store';
import { preferences, user } from '$lib/stores/user.store';
import { handleError } from '$lib/utils/handle-error';
import { setSupportBadgeVisibility } from '$lib/utils/purchase-utils';
@@ -22,7 +22,6 @@
import { mdiKey } from '@mdi/js';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
const { isPurchased } = purchaseStore;
let isServerProduct = $state(false);
let serverPurchaseInfo: LicenseResponseDto | null = $state(null);
@@ -53,7 +52,7 @@
};
onMount(async () => {
if (!$isPurchased) {
if (!authManager.isPurchased) {
return;
}
@@ -73,7 +72,7 @@
}
await deleteIndividualProductKey();
purchaseStore.setPurchaseStatus(false);
authManager.isPurchased = false;
} catch (error) {
handleError(error, $t('errors.failed_to_remove_product_key'));
}
@@ -92,21 +91,21 @@
}
await deleteServerProductKey();
purchaseStore.setPurchaseStatus(false);
authManager.isPurchased = false;
} catch (error) {
handleError(error, $t('errors.failed_to_remove_product_key'));
}
};
const onProductActivated = async () => {
purchaseStore.setPurchaseStatus(true);
authManager.isPurchased = true;
await checkPurchaseInfo();
};
</script>
<section class="my-4">
<div in:fade={{ duration: 500 }}>
{#if $isPurchased}
{#if authManager.isPurchased}
<!-- BADGE TOGGLE -->
<div class="mb-4">
<SettingSwitch

View File

@@ -3,12 +3,31 @@ import { page } from '$app/state';
import { eventManager } from '$lib/managers/event-manager.svelte';
import { Route } from '$lib/route';
import { isSharedLinkRoute } from '$lib/utils/navigation';
import { logout } from '@immich/sdk';
import { getAboutInfo, logout, type UserAdminResponseDto } from '@immich/sdk';
class AuthManager {
isPurchased = $state(false);
isSharedLink = $derived(isSharedLinkRoute(page.route?.id));
params = $derived(this.isSharedLink ? { key: page.params.key, slug: page.params.slug } : {});
constructor() {
eventManager.on({
AuthUserLoaded: (user) => this.onAuthUserLoaded(user),
});
}
private async onAuthUserLoaded(user: UserAdminResponseDto) {
if (user.license?.activatedAt) {
authManager.isPurchased = true;
return;
}
const serverInfo = await getAboutInfo().catch(() => undefined);
if (serverInfo?.licensed) {
authManager.isPurchased = true;
}
}
async logout() {
let redirectUri;
@@ -30,6 +49,7 @@ class AuthManager {
globalThis.location.href = redirectUri;
}
} finally {
this.isPurchased = false;
eventManager.emit('AuthLogout');
}
}

View File

@@ -1,16 +0,0 @@
import { readonly, writable } from 'svelte/store';
function createPurchaseStore() {
const isPurcharsed = writable(false);
function setPurchaseStatus(status: boolean) {
isPurcharsed.set(status);
}
return {
isPurchased: readonly(isPurcharsed),
setPurchaseStatus,
};
}
export const purchaseStore = createPurchaseStore();

View File

@@ -1,5 +1,4 @@
import { eventManager } from '$lib/managers/event-manager.svelte';
import { purchaseStore } from '$lib/stores/purchase.store';
import { type UserAdminResponseDto, type UserPreferencesResponseDto } from '@immich/sdk';
import { writable } from 'svelte/store';
@@ -13,7 +12,6 @@ export const preferences = writable<UserPreferencesResponseDto>();
export const resetSavedUser = () => {
user.set(undefined as unknown as UserAdminResponseDto);
preferences.set(undefined as unknown as UserPreferencesResponseDto);
purchaseStore.setPurchaseStatus(false);
};
eventManager.on({

View File

@@ -1,10 +1,9 @@
import { browser } from '$app/environment';
import { eventManager } from '$lib/managers/event-manager.svelte';
import { Route } from '$lib/route';
import { purchaseStore } from '$lib/stores/purchase.store';
import { preferences as preferences$, user as user$ } from '$lib/stores/user.store';
import { userInteraction } from '$lib/stores/user.svelte';
import { getAboutInfo, getMyPreferences, getMyUser, getStorage } from '@immich/sdk';
import { getMyPreferences, getMyUser, getStorage } from '@immich/sdk';
import { redirect } from '@sveltejs/kit';
import { DateTime } from 'luxon';
import { get } from 'svelte/store';
@@ -18,19 +17,12 @@ export const loadUser = async () => {
try {
let user = get(user$);
let preferences = get(preferences$);
let serverInfo;
if ((!user || !preferences) && hasAuthCookie()) {
[user, preferences, serverInfo] = await Promise.all([getMyUser(), getMyPreferences(), getAboutInfo()]);
[user, preferences] = await Promise.all([getMyUser(), getMyPreferences()]);
user$.set(user);
preferences$.set(preferences);
eventManager.emit('AuthUserLoaded', user);
// Check for license status
if (serverInfo.licensed || user.license?.activatedAt) {
purchaseStore.setPurchaseStatus(true);
}
}
return user;
} catch {

View File

@@ -4,8 +4,8 @@
import LicenseActivationSuccess from '$lib/components/shared-components/purchasing/purchase-activation-success.svelte';
import LicenseContent from '$lib/components/shared-components/purchasing/purchase-content.svelte';
import SupporterBadge from '$lib/components/shared-components/side-bar/supporter-badge.svelte';
import { authManager } from '$lib/managers/auth-manager.svelte';
import { Route } from '$lib/route';
import { purchaseStore } from '$lib/stores/purchase.store';
import { Alert, Container, Stack } from '@immich/ui';
import { mdiAlertCircleOutline } from '@mdi/js';
import { t } from 'svelte-i18n';
@@ -17,17 +17,16 @@
let { data }: Props = $props();
let showLicenseActivated = $state(false);
const { isPurchased } = purchaseStore;
</script>
<UserPageLayout title={$t('buy')}>
<UserPageLayout title={data.meta.title}>
<Container size="medium" center>
<Stack gap={4} class="mt-4">
{#if data.isActivated === false}
<Alert icon={mdiAlertCircleOutline} color="danger" title={$t('purchase_failed_activation')} />
{/if}
{#if $isPurchased}
{#if authManager.isPurchased}
<SupporterBadge logoSize="lg" centered />
{/if}

View File

@@ -1,4 +1,4 @@
import { purchaseStore } from '$lib/stores/purchase.store';
import { authManager } from '$lib/managers/auth-manager.svelte';
import { authenticate } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n';
import { activateProduct, getActivationKey } from '$lib/utils/license-utils';
@@ -21,7 +21,7 @@ export const load = (async ({ url }) => {
const response = await activateProduct(licenseKey, activationKey);
if (response.activatedAt !== '') {
isActivated = true;
purchaseStore.setPurchaseStatus(true);
authManager.isPurchased = true;
}
}
} catch (error) {