feat(web): Registration key linked to account status

This commit is contained in:
Zack Spear
2024-05-02 16:38:13 -07:00
committed by Zack Spear
parent 37b717b142
commit f28b7510fa
8 changed files with 189 additions and 11 deletions

View File

@@ -46,10 +46,10 @@ import type {
// '1111-1111-5GDB-123412341234' Starter.key = TkJCrVyXMLWWGKZF6TCEvf0C86UYI9KfUDSOm7JoFP19tOMTMgLKcJ6QIOt9_9Psg_t0yF-ANmzSgZzCo94ljXoPm4BESFByR0K7nyY9KVvU8szLEUcBUT3xC2adxLrAXFNxiPeK-mZqt34n16uETKYvLKL_Sr5_JziG5L5lJFBqYZCPmfLMiguFo1vp0xL8pnBH7q8bYoBnePrAcAVb9mAGxFVPEInSPkMBfC67JLHz7XY1Y_K5bYIq3go9XPtLltJ53_U4BQiMHooXUBJCKXodpqoGxq0eV0IhNEYdauAhnTsG90qmGZig0hZalQ0soouc4JZEMiYEcZbn9mBxPg
const state: ServerState = 'TRIAL';
const currentFlashGuid = '1111-1111-NUIK-TEST1234ZACK'; // this is the flash drive that's been booted from
const regGuid = '1111-1111-NUIK-TEST1234ZACK'; // this guid is registered in key server
const keyfileBase64 = ''; // @todo raycast download key to base64
const state: ServerState = 'BASIC';
const currentFlashGuid = '1111-1111-CFXF-TEST1234ZACK'; // this is the flash drive that's been booted from
const regGuid = '1111-1111-CFXF-TEST1234ZACK'; // this guid is registered in key server
const keyfileBase64 = 'asdf'; // @todo raycast download key to base64
// const randomGuid = `1111-1111-${makeid(4)}-123412341234`; // this guid is registered in key server
// const newGuid = `1234-1234-${makeid(4)}-123412341234`; // this is a new USB, not registered

View File

@@ -32,6 +32,7 @@ import type { RegistrationItemProps } from '~/types/registration';
import KeyActions from '~/components/KeyActions.vue';
import RegistrationReplaceCheck from '~/components/Registration/ReplaceCheck.vue';
import RegistrationKeyLinkedStatus from '~/components/Registration/KeyLinkedStatus.vue';
import RegistrationUpdateExpirationAction from '~/components/Registration/UpdateExpirationAction.vue';
import UserProfileUptimeExpire from '~/components/UserProfile/UptimeExpire.vue';
@@ -39,6 +40,8 @@ const { t } = useI18n();
const replaceRenewCheckStore = useReplaceRenewStore();
const serverStore = useServerStore();
const { keyLinkedStatus } = storeToRefs(replaceRenewCheckStore);
const {
computedArray,
arrayWarning,
@@ -103,7 +106,7 @@ const subheading = computed(() => {
const showTrialExpiration = computed((): boolean => state.value === 'TRIAL' || state.value === 'EEXPIRED');
const showUpdateEligibility = computed((): boolean => !!(regExp.value));
const keyInstalled = computed((): boolean => !!(!stateDataError.value && state.value !== 'ENOKEYFILE'));
const showTransferStatus = computed((): boolean => !!(keyInstalled.value && guid.value && !showTrialExpiration.value));
const showLinkedAndTransferStatus = computed((): boolean => !!(keyInstalled.value && guid.value && !showTrialExpiration.value));
// filter out renew action and only display other key actions…renew is displayed in RegistrationUpdateExpirationAction
const showFilteredKeyActions = computed((): boolean => !!(keyActions.value && keyActions.value?.filter(action => !['renew'].includes(action.name)).length > 0));
@@ -189,13 +192,20 @@ const items = computed((): RegistrationItemProps[] => {
: t('{0} out of {1} devices', [deviceCount.value, computedRegDevs.value === -1 ? t('unlimited') : computedRegDevs.value]),
}]
: []),
...(showTransferStatus.value
...(showLinkedAndTransferStatus.value
? [{
label: t('Transfer License to New Flash'),
component: RegistrationReplaceCheck,
componentProps: { t },
}]
: []),
...(regTo.value && showLinkedAndTransferStatus.value
? [{
label: t('Linked to Unraid.net account'),
component: RegistrationKeyLinkedStatus,
componentProps: { t },
}]
: []),
...(showFilteredKeyActions.value
? [{

View File

@@ -35,7 +35,13 @@ const evenBgColor = computed(() => {
class="leading-normal sm:col-span-3"
:class="!label && 'sm:col-start-2'"
>
<span v-if="text" class="select-all" :class="!error ? 'opacity-75' : ''">
<span
v-if="text"
class="select-all"
:class="{
'opacity-75': !error,
}"
>
{{ text }}
</span>
<template v-if="$slots['right']">

View File

@@ -0,0 +1,73 @@
<script setup lang="ts">
import {
ArrowTopRightOnSquareIcon,
ArrowPathIcon,
LinkIcon,
} from '@heroicons/vue/24/solid';
import { storeToRefs } from 'pinia';
import { useAccountStore } from '~/store/account';
import { useReplaceRenewStore } from '~/store/replaceRenew';
const accountStore = useAccountStore();
const replaceRenewStore = useReplaceRenewStore();
const { keyLinkedStatus, keyLinkedOutput } = storeToRefs(replaceRenewStore);
defineProps<{
t: any;
}>();
</script>
<template>
<div class="flex flex-wrap items-center justify-between gap-8px">
<BrandButton
v-if="keyLinkedStatus !== 'linked' && keyLinkedStatus !== 'checking'"
btn-style="none"
:no-padding="true"
:title="t('Refresh')"
class="group"
@click="replaceRenewStore.check(true)"
>
<UiBadge
v-if="keyLinkedOutput"
:color="keyLinkedOutput.color"
:icon="keyLinkedOutput.icon"
:icon-right="ArrowPathIcon"
size="16px"
>
{{ t(keyLinkedOutput.text) }}
</UiBadge>
</BrandButton>
<UiBadge
v-else
:color="keyLinkedOutput.color"
:icon="keyLinkedOutput.icon"
size="16px"
>
{{ t(keyLinkedOutput.text) }}
</UiBadge>
<span class="inline-flex flex-wrap-items-start gap-8px">
<BrandButton
v-if="keyLinkedStatus === 'notLinked'"
btn-style="underline"
:external="true"
:icon="LinkIcon"
:icon-right="ArrowTopRightOnSquareIcon"
:text="t('Link Key')"
:title="t('Learn more and link your key to your account')"
class="text-14px"
@click="accountStore.linkKey"
/>
<BrandButton
v-else
btn-style="underline"
:external="true"
:icon-right="ArrowTopRightOnSquareIcon"
:text="t('Learn More')"
class="text-14px"
@click="accountStore.manage"
/>
</span>
</div>
</template>

View File

@@ -17,7 +17,7 @@ defineProps<{
</script>
<template>
<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="!replaceStatusOutput"
:icon="KeyIcon"

View File

@@ -18,6 +18,7 @@ export const startTrial = async (payload: StartTrialPayload): Promise<StartTrial
export interface ValidateGuidResponse {
hasNewerKeyfile : boolean;
linked: boolean;
purchaseable: true;
registered: false;
replaceable: false;

View File

@@ -5,6 +5,7 @@ import { defineStore, createPinia, setActivePinia } from 'pinia';
import { CONNECT_SIGN_IN, CONNECT_SIGN_OUT } from './account.fragment';
import { useCallbackStore } from '~/store/callbackActions';
import { useErrorsStore } from '~/store/errors';
import { useReplaceRenewStore } from '~/store/replaceRenew';
import { useServerStore } from '~/store/server';
import { useUnraidApiStore } from '~/store/unraidApi';
import { ACCOUNT_CALLBACK } from '~/helpers/urls';
@@ -24,6 +25,7 @@ export interface ConnectSignInMutationPayload {
export const useAccountStore = defineStore('account', () => {
const callbackStore = useCallbackStore();
const errorsStore = useErrorsStore();
const replaceRenewStore = useReplaceRenewStore();
const serverStore = useServerStore();
const unraidApiStore = useUnraidApiStore();
@@ -84,6 +86,40 @@ export const useAccountStore = defineStore('account', () => {
inIframe.value ? 'newTab' : undefined,
);
};
const myKeys = async () => {
/**
* Purge the validation response so we can start fresh after the user has linked their key
*/
await replaceRenewStore.purgeValidationResponse();
callbackStore.send(
ACCOUNT_CALLBACK.toString(),
[{
server: {
...serverAccountPayload.value,
},
type: 'myKeys',
}],
inIframe.value ? 'newTab' : undefined,
);
};
const linkKey = async () => {
/**
* Purge the validation response so we can start fresh after the user has linked their key
*/
await replaceRenewStore.purgeValidationResponse();
callbackStore.send(
ACCOUNT_CALLBACK.toString(),
[{
server: {
...serverAccountPayload.value,
},
type: 'linkKey',
}],
inIframe.value ? 'newTab' : undefined,
);
};
const recover = () => {
callbackStore.send(
ACCOUNT_CALLBACK.toString(),
@@ -267,6 +303,7 @@ export const useAccountStore = defineStore('account', () => {
// Getters
accountActionType,
// Actions
linkKey,
manage,
recover,
replace,

View File

@@ -6,6 +6,7 @@
*/
import {
CheckCircleIcon,
ExclamationCircleIcon,
XCircleIcon,
ShieldExclamationIcon,
} from '@heroicons/vue/24/solid';
@@ -54,6 +55,47 @@ export const useReplaceRenewStore = defineStore('replaceRenewCheck', () => {
cause?: unknown;
} | null>(null);
const keyLinkedStatus = ref<'checking' | 'linked' | 'notLinked' | 'error' | 'ready'>('ready');
const setKeyLinked = (value: typeof keyLinkedStatus.value) => {
keyLinkedStatus.value = value;
};
const keyLinkedOutput = computed((): UiBadgePropsExtended => {
// text values are translated in the component
switch (keyLinkedStatus.value) {
case 'checking':
return {
color: 'gamma',
icon: BrandLoadingWhite,
text: 'Checking...',
};
case 'linked':
return {
color: 'green',
icon: CheckCircleIcon,
text: 'Linked',
};
case 'notLinked':
return {
color: 'yellow',
icon: ExclamationCircleIcon,
text: 'Not Linked',
};
case 'error':
return {
color: 'red',
icon: ShieldExclamationIcon,
text: error.value?.message || 'Unknown error',
};
case 'ready':
default:
return {
color: 'gray',
icon: ExclamationCircleIcon,
text: 'Unknown',
};
}
});
const renewStatus = ref<'checking' | 'error' | 'installing' | 'installed' | 'ready'>('ready');
const setRenewStatus = (status: typeof renewStatus.value) => {
renewStatus.value = status;
@@ -128,7 +170,7 @@ export const useReplaceRenewStore = defineStore('replaceRenewCheck', () => {
}
};
const check = async () => {
const check = async (skipCache: boolean = false) => {
if (!guid.value) {
setReplaceStatus('error');
error.value = { name: 'Error', message: 'Flash GUID required to check replacement status' };
@@ -139,9 +181,14 @@ export const useReplaceRenewStore = defineStore('replaceRenewCheck', () => {
}
try {
// validate the cache first - will purge if it's too old
await validateCache();
if (skipCache) {
await purgeValidationResponse();
} else {
// validate the cache first - will purge if it's too old
await validateCache();
}
setKeyLinked('checking');
setReplaceStatus('checking');
error.value = null;
/**
@@ -158,6 +205,7 @@ export const useReplaceRenewStore = defineStore('replaceRenewCheck', () => {
}
setReplaceStatus(response?.replaceable ? 'eligible' : 'ineligible');
setKeyLinked(response?.linked ? 'linked' : 'notLinked');
/** cache the response to prevent repeated POSTs in the session */
if ((replaceStatus.value === 'eligible' || replaceStatus.value === 'ineligible') && !validationResponse.value) {
@@ -197,11 +245,14 @@ export const useReplaceRenewStore = defineStore('replaceRenewCheck', () => {
return {
// state
keyLinkedStatus,
keyLinkedOutput,
renewStatus,
replaceStatus,
replaceStatusOutput,
// actions
check,
purgeValidationResponse,
setReplaceStatus,
setRenewStatus,
};