refactor: purchase store (#25734)

This commit is contained in:
Jason Rasmussen 2026-02-12 13:32:17 -05:00 committed by GitHub
parent 6c0c4b3dda
commit 5bf4e9595c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 39 additions and 49 deletions

View file

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

View file

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

View file

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

View file

@ -3,12 +3,31 @@ import { page } from '$app/state';
import { eventManager } from '$lib/managers/event-manager.svelte'; import { eventManager } from '$lib/managers/event-manager.svelte';
import { Route } from '$lib/route'; import { Route } from '$lib/route';
import { isSharedLinkRoute } from '$lib/utils/navigation'; import { isSharedLinkRoute } from '$lib/utils/navigation';
import { logout } from '@immich/sdk'; import { getAboutInfo, logout, type UserAdminResponseDto } from '@immich/sdk';
class AuthManager { class AuthManager {
isPurchased = $state(false);
isSharedLink = $derived(isSharedLinkRoute(page.route?.id)); isSharedLink = $derived(isSharedLinkRoute(page.route?.id));
params = $derived(this.isSharedLink ? { key: page.params.key, slug: page.params.slug } : {}); 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() { async logout() {
let redirectUri; let redirectUri;
@ -30,6 +49,7 @@ class AuthManager {
globalThis.location.href = redirectUri; globalThis.location.href = redirectUri;
} }
} finally { } finally {
this.isPurchased = false;
eventManager.emit('AuthLogout'); 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 { eventManager } from '$lib/managers/event-manager.svelte';
import { purchaseStore } from '$lib/stores/purchase.store';
import { type UserAdminResponseDto, type UserPreferencesResponseDto } from '@immich/sdk'; import { type UserAdminResponseDto, type UserPreferencesResponseDto } from '@immich/sdk';
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
@ -13,7 +12,6 @@ export const preferences = writable<UserPreferencesResponseDto>();
export const resetSavedUser = () => { export const resetSavedUser = () => {
user.set(undefined as unknown as UserAdminResponseDto); user.set(undefined as unknown as UserAdminResponseDto);
preferences.set(undefined as unknown as UserPreferencesResponseDto); preferences.set(undefined as unknown as UserPreferencesResponseDto);
purchaseStore.setPurchaseStatus(false);
}; };
eventManager.on({ eventManager.on({

View file

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

View file

@ -4,8 +4,8 @@
import LicenseActivationSuccess from '$lib/components/shared-components/purchasing/purchase-activation-success.svelte'; import LicenseActivationSuccess from '$lib/components/shared-components/purchasing/purchase-activation-success.svelte';
import LicenseContent from '$lib/components/shared-components/purchasing/purchase-content.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 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 { Route } from '$lib/route';
import { purchaseStore } from '$lib/stores/purchase.store';
import { Alert, Container, Stack } from '@immich/ui'; import { Alert, Container, Stack } from '@immich/ui';
import { mdiAlertCircleOutline } from '@mdi/js'; import { mdiAlertCircleOutline } from '@mdi/js';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
@ -17,17 +17,16 @@
let { data }: Props = $props(); let { data }: Props = $props();
let showLicenseActivated = $state(false); let showLicenseActivated = $state(false);
const { isPurchased } = purchaseStore;
</script> </script>
<UserPageLayout title={$t('buy')}> <UserPageLayout title={data.meta.title}>
<Container size="medium" center> <Container size="medium" center>
<Stack gap={4} class="mt-4"> <Stack gap={4} class="mt-4">
{#if data.isActivated === false} {#if data.isActivated === false}
<Alert icon={mdiAlertCircleOutline} color="danger" title={$t('purchase_failed_activation')} /> <Alert icon={mdiAlertCircleOutline} color="danger" title={$t('purchase_failed_activation')} />
{/if} {/if}
{#if $isPurchased} {#if authManager.isPurchased}
<SupporterBadge logoSize="lg" centered /> <SupporterBadge logoSize="lg" centered />
{/if} {/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 { authenticate } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n'; import { getFormatter } from '$lib/utils/i18n';
import { activateProduct, getActivationKey } from '$lib/utils/license-utils'; import { activateProduct, getActivationKey } from '$lib/utils/license-utils';
@ -21,7 +21,7 @@ export const load = (async ({ url }) => {
const response = await activateProduct(licenseKey, activationKey); const response = await activateProduct(licenseKey, activationKey);
if (response.activatedAt !== '') { if (response.activatedAt !== '') {
isActivated = true; isActivated = true;
purchaseStore.setPurchaseStatus(true); authManager.isPurchased = true;
} }
} }
} catch (error) { } catch (error) {