Compare commits

...

1 Commits

Author SHA1 Message Date
Zack Spear
ab1abc4ffe feat: re-enable client-side replace with new key check, adds manual re-check 2025-03-13 18:32:01 -07:00
5 changed files with 141 additions and 64 deletions

View File

@@ -24,7 +24,7 @@ defineProps<{
@click="replaceRenewStore.check"
/>
<Badge v-else :variant="replaceStatusOutput.variant" :icon="replaceStatusOutput.icon" size="md">
<Badge v-else :variant="replaceStatusOutput.variant" :icon="replaceStatusOutput.icon" size="md" class="self-center">
{{ t(replaceStatusOutput.text ?? 'Unknown') }}
</Badge>

View File

@@ -1,15 +1,17 @@
<script setup lang="ts">
import { storeToRefs } from 'pinia';
import { storeToRefs } from "pinia";
import { computed, h } from "vue";
import { ArrowPathIcon, ArrowTopRightOnSquareIcon } from '@heroicons/vue/24/solid';
import { BrandButton } from '@unraid/ui';
import { DOCS_REGISTRATION_LICENSING } from '~/helpers/urls';
import { ArrowPathIcon, ArrowTopRightOnSquareIcon } from "@heroicons/vue/24/solid";
import { BrandButton, BrandLoading } from "@unraid/ui";
import type { ComposerTranslation } from 'vue-i18n';
import { DOCS_REGISTRATION_LICENSING } from "~/helpers/urls";
import useDateTimeHelper from '~/composables/dateTime';
import { useReplaceRenewStore } from '~/store/replaceRenew';
import { useServerStore } from '~/store/server';
import type { ComposerTranslation } from "vue-i18n";
import useDateTimeHelper from "~/composables/dateTime";
import { useReplaceRenewStore } from "~/store/replaceRenew";
import { useServerStore } from "~/store/server";
export interface Props {
t: ComposerTranslation;
@@ -20,15 +22,29 @@ const props = defineProps<Props>();
const replaceRenewStore = useReplaceRenewStore();
const serverStore = useServerStore();
const { renewStatus } = storeToRefs(replaceRenewStore);
const { dateTimeFormat, regExp, regUpdatesExpired, renewAction } = storeToRefs(serverStore);
const { renewStatus, validationResponseTimestamp } = storeToRefs(replaceRenewStore);
const { dateTimeFormat, regExp, regUpdatesExpired, renewAction } = storeToRefs(
serverStore
);
const reload = () => {
window.location.reload();
};
const { outputDateTimeReadableDiff: readableDiffRegExp, outputDateTimeFormatted: formattedRegExp } =
useDateTimeHelper(dateTimeFormat.value, props.t, true, regExp.value);
const {
outputDateTimeReadableDiff: readableDiffRegExp,
outputDateTimeFormatted: formattedRegExp,
} = useDateTimeHelper(dateTimeFormat.value, props.t, true, regExp.value);
const {
outputDateTimeReadableDiff: readableDiffValidationResponseTimestamp,
outputDateTimeFormatted: formattedValidationResponseTimestamp,
} = useDateTimeHelper(
dateTimeFormat.value,
props.t,
false,
validationResponseTimestamp.value ?? undefined
);
const output = computed(() => {
if (!regExp.value) {
@@ -36,29 +52,68 @@ const output = computed(() => {
}
return {
text: regUpdatesExpired.value
? props.t('Ineligible for feature updates released after {0}', [formattedRegExp.value])
: props.t('Eligible for free feature updates until {0}', [formattedRegExp.value]),
? props.t("Ineligible for feature updates released after {0}", [
formattedRegExp.value,
])
: props.t("Eligible for free feature updates until {0}", [formattedRegExp.value]),
title: regUpdatesExpired.value
? props.t('Ineligible as of {0}', [readableDiffRegExp.value])
: props.t('Eligible for free feature updates for {0}', [readableDiffRegExp.value]),
? props.t("Ineligible as of {0}", [readableDiffRegExp.value])
: props.t("Eligible for free feature updates for {0}", [readableDiffRegExp.value]),
};
});
const showCheckTimestamp = computed(() => {
return (
validationResponseTimestamp.value && validationResponseTimestamp.value > regExp.value
);
});
const reloadPage = () => {
window.location.reload();
};
const BrandLoadingIcon = () => h(BrandLoading, { size: 'sm' });
const statusContent = computed(() => {
switch (renewStatus.value) {
case "installed":
return {
text: props.t(
"Your license key was automatically renewed and installed. Reload the page to see updated details."
),
action: {
icon: ArrowPathIcon,
title: "Reload Page",
onClick: reloadPage,
},
};
case "checking":
return {
component: BrandLoadingIcon,
text: props.t("Checking for extended license..."),
};
case "error":
return { text: props.t("Error checking for extended license.") };
case "ready":
return {
text: showCheckTimestamp.value ? `Last checked: ${formattedValidationResponseTimestamp.value}` : null,
action: {
icon: ArrowPathIcon,
title: "Check Again for Extended License",
onClick: () => replaceRenewStore.check(true), // consider debouncing this
},
};
default:
return null;
}
});
</script>
<template>
<div v-if="output" class="flex flex-col gap-8px">
<RegistrationUpdateExpiration :t="t" />
<p class="text-14px opacity-90">
<template v-if="renewStatus === 'installed'">
{{
t(
'Your license key was automatically renewed and installed. Reload the page to see updated details.'
)
}}
</template>
</p>
<div class="flex flex-wrap items-start justify-between gap-8px">
<div class="flex flex-wrap items-center justify-between gap-8px">
<BrandButton
v-if="renewStatus === 'installed'"
:icon="ArrowPathIcon"
@@ -87,6 +142,21 @@ const output = computed(() => {
:text="t('Learn More')"
class="text-14px"
/>
<p class="text-14px opacity-90 w-full flex flex-wrap gap-8px items-center">
<template v-if="statusContent">
<BrandButton
v-if="statusContent.action"
variant="underline"
:icon="statusContent.action.icon"
:title="statusContent.action.title"
size="12px"
@click="statusContent.action.onClick"
/>
<component :is="statusContent.component" v-if="statusContent.component" />
<span v-if="statusContent.text">{{ statusContent.text }}</span>
</template>
</p>
</div>
</div>
</template>

View File

@@ -1,12 +1,12 @@
<script setup lang="ts">
import { storeToRefs } from 'pinia';
import { storeToRefs } from "pinia";
import { TransitionRoot } from '@headlessui/vue';
import { TransitionRoot } from "@headlessui/vue";
import type { ComposerTranslation } from 'vue-i18n';
import type { ComposerTranslation } from "vue-i18n";
import { useDropdownStore } from '~/store/dropdown';
import { useServerStore } from '~/store/server';
import { useDropdownStore } from "~/store/dropdown";
import { useServerStore } from "~/store/server";
defineProps<{ t: ComposerTranslation }>();
@@ -15,7 +15,7 @@ const dropdownStore = useDropdownStore();
const { dropdownVisible } = storeToRefs(dropdownStore);
const { state } = storeToRefs(useServerStore());
const showLaunchpad = computed(() => state.value === 'ENOKEYFILE');
const showLaunchpad = computed(() => state.value === "ENOKEYFILE");
</script>
<template>
@@ -28,9 +28,7 @@ const showLaunchpad = computed(() => state.value === 'ENOKEYFILE');
leave-from="opacity-100"
leave-to="opacity-0 translate-y-[16px]"
>
<UpcDropdownWrapper
class="DropdownWrapper_blip text-foreground absolute z-30 top-full right-0 transition-all"
>
<UpcDropdownWrapper>
<UpcDropdownLaunchpad v-if="showLaunchpad" :t="t" />
<UpcDropdownContent v-else :t="t" />
</UpcDropdownWrapper>

View File

@@ -2,7 +2,7 @@
</script>
<template>
<nav class="flex flex-col gap-y-8px p-8px bg-popover rounded-lg shadow-xl shadow-orange/10">
<nav class="text-foreground absolute z-30 top-full right-0 flex flex-col gap-y-8px p-8px bg-popover rounded-lg shadow-xl shadow-orange/10">
<slot />
</nav>
</template>

View File

@@ -17,16 +17,16 @@ import { BrandLoading } from '@unraid/ui';
import type { BadgeProps } from '@unraid/ui';
import type {
// type KeyLatestResponse,
KeyLatestResponse,
ValidateGuidResponse,
} from '~/composables/services/keyServer';
import type { WretchError } from 'wretch';
import {
// keyLatest,
keyLatest,
validateGuid,
} from '~/composables/services/keyServer';
// import { useCallbackStore } from '~/store/callbackActions';
import { useCallbackStore } from '~/store/callbackActions';
import { useServerStore } from '~/store/server';
/**
@@ -44,12 +44,12 @@ interface CachedValidationResponse extends ValidateGuidResponse {
timestamp: number;
}
const BrandLoadingIcon = () => h(BrandLoading, { variant: 'white' });
const BrandLoadingIcon = () => h(BrandLoading, { size: 'sm' });
export const REPLACE_CHECK_LOCAL_STORAGE_KEY = 'unraidReplaceCheck';
export const useReplaceRenewStore = defineStore('replaceRenewCheck', () => {
// const callbackStore = useCallbackStore();
const callbackStore = useCallbackStore();
const serverStore = useServerStore();
const guid = computed(() => serverStore.guid);
@@ -156,6 +156,8 @@ export const useReplaceRenewStore = defineStore('replaceRenewCheck', () => {
: undefined
);
const validationResponseTimestamp = computed<number | null>(() => validationResponse.value?.timestamp ?? null);
const purgeValidationResponse = async () => {
validationResponse.value = undefined;
await sessionStorage.removeItem(REPLACE_CHECK_LOCAL_STORAGE_KEY);
@@ -190,6 +192,8 @@ export const useReplaceRenewStore = defineStore('replaceRenewCheck', () => {
error.value = { name: 'Error', message: 'Keyfile required to check replacement status' };
}
console.log('[ReplaceCheck.check]');
try {
if (skipCache) {
await purgeValidationResponse();
@@ -200,6 +204,7 @@ export const useReplaceRenewStore = defineStore('replaceRenewCheck', () => {
setKeyLinked('checking');
setReplaceStatus('checking');
setRenewStatus('checking');
error.value = null;
/**
* If the session already has a validation response, use that instead of making a new request
@@ -222,35 +227,38 @@ export const useReplaceRenewStore = defineStore('replaceRenewCheck', () => {
(replaceStatus.value === 'eligible' || replaceStatus.value === 'ineligible') &&
!validationResponse.value
) {
validationResponse.value = {
key: keyfileShort.value,
timestamp: Date.now(),
...response,
};
sessionStorage.setItem(
REPLACE_CHECK_LOCAL_STORAGE_KEY,
JSON.stringify({
key: keyfileShort.value,
timestamp: Date.now(),
...response,
})
JSON.stringify(validationResponse.value)
);
}
// if (response?.hasNewerKeyfile) {
// setRenewStatus('checking');
if (!response?.hasNewerKeyfile) {
setRenewStatus('ready');
} else if (response?.hasNewerKeyfile) {
setRenewStatus('checking');
// const keyLatestResponse: KeyLatestResponse = await keyLatest({
// keyfile: keyfile.value,
// });
const keyLatestResponse: KeyLatestResponse = await keyLatest({
keyfile: keyfile.value,
});
// if (keyLatestResponse?.license) {
// callbackStore.send(
// window.location.href,
// [{
// keyUrl: keyLatestResponse.license,
// type: 'renew',
// }],
// undefined,
// 'forUpc',
// );
// }
// }
if (keyLatestResponse?.license) {
callbackStore.send(
window.location.href,
[{
keyUrl: keyLatestResponse.license,
type: 'renew',
}],
undefined,
'forUpc',
);
}
}
} catch (err) {
const catchError = err as WretchError;
setReplaceStatus('error');
@@ -266,6 +274,7 @@ export const useReplaceRenewStore = defineStore('replaceRenewCheck', () => {
renewStatus,
replaceStatus,
replaceStatusOutput,
validationResponseTimestamp,
// actions
check,
purgeValidationResponse,