feat(web): registration component ui / ux

This commit is contained in:
Zack Spear
2023-09-26 17:40:18 -07:00
committed by Zack Spear
parent f5b0ca63e8
commit 6c98369719
12 changed files with 291 additions and 46 deletions
+26 -2
View File
@@ -30,7 +30,31 @@ const randomGuid = `1111-1111-${makeid(4)}-123412341234`; // this guid is regist
// EBLACKLISTED2
// ENOCONN
const state: ServerState = 'TRIAL';
const regTy = 'Trial';
let regTy = '';
switch (state) {
// @ts-ignore
case 'EEXPIRED':
// @ts-ignore
case 'ENOCONN':
// @ts-ignore
case 'TRIAL':
regTy = 'Trial';
break;
// @ts-ignore
case 'BASIC':
// @ts-ignore
case 'PLUS':
// @ts-ignore
case 'PRO':
// @ts-ignore
case 'STARTER':
// @ts-ignore
case 'UNLEASHED':
// @ts-ignore
case 'LIFETIME':
regTy = state.charAt(0).toUpperCase() + state.substring(1).toLowerCase(); // title case
break;
}
const uptime = Date.now() - 60 * 60 * 1000; // 1 hour ago
const oneHourFromNow = Date.now() + 60 * 60 * 1000; // 1 hour from now
@@ -77,7 +101,7 @@ export const serverState: Server = {
bgColor: '',
descriptionShow: true,
metaColor: '',
name: 'white',
name: 'black',
textColor: ''
},
uptime,
+69 -22
View File
@@ -15,6 +15,8 @@ else
echo "Third party plugins found - PLEASE CHECK YOUR UNRAID NOTIFICATIONS AND WAIT FOR THE MESSAGE THAT IT IS SAFE TO REBOOT!"
fi
*/
import 'tailwindcss/tailwind.css';
import '~/assets/main.css';
import {
ShieldCheckIcon,
ShieldExclamationIcon,
@@ -24,12 +26,12 @@ import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import { useServerStore } from '~/store/server';
import 'tailwindcss/tailwind.css';
import '~/assets/main.css';
import { RegistrationItemProps } from '~/types/registration';
import KeyActions from '~/components/KeyActions.vue';
import RegistrationReplaceCheck from '~/components/Registration/ReplaceCheck.vue';
import RegistrationUpgradeExpiration from '~/components/Registration/UpgradeExpiration.vue';
import UserProfileUptimeExpire from '~/components/UserProfile/UptimeExpire.vue';
const { t } = useI18n();
@@ -61,41 +63,83 @@ const {
stateDataError,
} = storeToRefs(serverStore);
const devicesAvailable = computed(() => {
const devicesAvailable = computed((): number => {
switch(regTy.value) {
case 'Starter':
return 4;
case 'Basic':
return 6;
case 'Plus':
return 12;
case 'Unleashed':
case 'Lifetime':
case 'Pro':
case 'Trial':
return 'unlimited';
return 9999;
default:
return 0;
}
});
const items = computed(() => {
const items = computed((): RegistrationItemProps[] => {
return [
...(regTy.value ? [{ label: t('License key type'), text: regTy.value }] : []),
...(regTo.value ? [ { label: t('Registered to'), text: regTo.value }] : []),
...(regTo.value ? [{ label: t('Registered on'), text: dayjs(regTm.value).format('YYYY-MM-DD HH:mm')}] : []),
/**
* @todo factor in grandfathered users and display a different message
*/
...(regUpdExpAt.value
? [{
...(regTy.value ? [{
label: t('License key type'),
text: regTy.value,
}] : []),
...(state.value === 'TRIAL' || state.value === 'EEXPIRED' ? [{
error: state.value === 'EEXPIRED',
label: t('Trial expiration'),
component: UserProfileUptimeExpire,
componentProps: {
forExpire: true,
shortText: true,
t: t,
},
componentOpacity: true,
}] : []),
...(regTo.value ? [{
label: t('Registered to'),
text: regTo.value,
}] : []),
...(regTo.value && regTm.value ? [{
label: t('Registered on'),
text: dayjs(regTm.value).format('YYYY-MM-DD HH:mm'),
}] : []),
...(regUpdExpAt.value && (state.value === 'STARTER' || state.value === 'UNLEASHED') ? [{
error: regUpdExpired.value,
label: t('OS Update Eligibility'),
component: RegistrationUpgradeExpiration,
componentProps: { t: t },
}]
: []),
...(state.value === 'EGUID' ? [{ label: t('Registered GUID'), text: regGuid.value }] : [] ),
{ label: t('Flash GUID'), text: guid.value },
{ label: t('Flash Vendor'), text: flashVendor.value },
{ label: t('Flash Product'), text: flashProduct.value },
{ label: t('Attached Storage Devices'), text: t('{0} out of {1} devices', [deviceCount.value, devicesAvailable.value]) },
...(regUpdExpAt.value
? [{
...(state.value === 'EGUID' ? [{
label: t('Registered GUID'),
text: regGuid.value,
}] : [] ),
...(guid.value ? [{
label: t('Flash GUID'),
text: guid.value,
}] : [] ),
...(flashVendor.value ? [{
label: t('Flash Vendor'),
text: flashVendor.value,
}] : [] ),
...(flashProduct.value ? [{
label: t('Flash Product'),
text: flashProduct.value,
}] : [] ),
...(!stateDataError.value ? [{
error: deviceCount.value > devicesAvailable.value,
label: t('Attached Storage Devices'),
text: t('{0} out of {1} devices', [deviceCount.value, devicesAvailable.value > 12 ? t('unlimited') : devicesAvailable.value]),
}] : []),
...(!stateDataError.value && guid.value ? [{
label: t('Key Replacement Eligibility'),
component: RegistrationReplaceCheck,
componentProps: { t: t },
}] : []),
...(keyActions.value ? [{
label: t('License key actions'),
component: KeyActions,
componentProps: { t: t },
@@ -135,7 +179,10 @@ const items = computed(() => {
:text="item.text"
>
<template v-if="item.component" #right>
<component :is="item.component" v-bind="item.componentProps" />
<component
:is="item.component"
v-bind="item.componentProps"
:class="[item.componentOpacity && !item.error ? 'opacity-75' : '']" />
</template>
</RegistrationItem>
</dl>
+6 -15
View File
@@ -1,17 +1,12 @@
<script setup lang="ts">
import { ShieldExclamationIcon } from '@heroicons/vue/24/solid';
import { storeToRefs } from 'pinia';
import { useThemeStore } from '~/store/theme';
export interface Props {
error?: boolean;
label?: string;
text?: number | string | undefined;
}
const props = withDefaults(defineProps<Props>(), {
import { useThemeStore } from '~/store/theme';
import { RegistrationItemProps } from '~/types/registration';
withDefaults(defineProps<RegistrationItemProps>(), {
error: false,
label: '',
text: '',
});
const { darkMode } = storeToRefs(useThemeStore());
@@ -19,10 +14,6 @@ const { darkMode } = storeToRefs(useThemeStore());
const evenBgColor = computed(() => {
return darkMode.value ? 'even:bg-grey-darkest' : 'even:bg-black/5';
});
onMounted(() => {
console.debug('[Item.onMounted]', props);
})
</script>
<template>
@@ -31,13 +22,13 @@ onMounted(() => {
!error && evenBgColor,
error && 'text-white bg-unraid-red',
]"
class="text-16px p-16px sm:px-20px sm:grid sm:grid-cols-3 sm:gap-16px items-start"
class="text-16px p-16px grid grid-cols-1 gap-4px sm:px-20px sm:grid-cols-3 sm:gap-16px items-start"
>
<dt class="font-semibold flex flex-row justify-start items-center gap-x-8px">
<ShieldExclamationIcon v-if="error" class="w-16px h-16px fill-current" />
<span>{{ label }}</span>
</dt>
<dd class="mt-4px leading-normal sm:col-span-2 sm:mt-0">
<dd class="leading-normal sm:col-span-2">
<span v-if="text" class="opacity-75 select-all">{{ text }}</span>
<template v-if="$slots['right']">
<slot name="right"></slot>
@@ -0,0 +1,111 @@
<script setup lang="ts">
import {
CheckCircleIcon,
KeyIcon,
XCircleIcon,
ShieldExclamationIcon,
} from '@heroicons/vue/24/solid';
import { storeToRefs } from 'pinia';
import type { WretchError } from 'wretch';
import { validateGuid, type ValidateGuidPayload } from '~/composables/services/keyServer';
import { useServerStore } from '~/store/server';
import BrandLoadingWhite from '~/components/Brand/LoadingWhite.vue';
import { storeKeyNameFromField } from '@apollo/client/utilities';
const props = defineProps<{
t: any;
}>();
const { guid } = storeToRefs(useServerStore());
const error = ref<{
name: string;
message: string;
stack?: string | undefined;
cause?: unknown;
} | null>(null);
const status = ref<'checking' | 'eligible' | 'error' | 'ineligible' | 'ready'>(guid.value ? 'ready' : 'error');
const statusOutput = computed(() => {
switch (status.value) {
case 'eligible':
return {
classes: 'text-green-500',
icon: CheckCircleIcon,
text: props.t('Eligible'),
};
case 'ineligible':
return {
classes: 'text-red-500',
icon: XCircleIcon,
text: props.t('Ineligible'),
};
case 'error':
return {
classes: 'text-red-500',
icon: ShieldExclamationIcon,
text: error.value?.message || 'Unknown error',
};
default: return null;
}
});
const validationResponse = ref<ValidateGuidPayload | undefined>(sessionStorage.getItem('replaceCheck') ? JSON.parse(sessionStorage.getItem('replaceCheck') as string) : undefined);
const check = async () => {
if (!guid.value) {
status.value = 'error';
error.value = { name: 'Error', message: props.t('Flash GUID required') };
}
try {
status.value = 'checking';
error.value = null;
const response: ValidateGuidPayload = await validateGuid({ guid: guid.value }).json();
sessionStorage.setItem('replaceCheck', JSON.stringify(response));
status.value = response?.replaceable ? 'eligible' : 'ineligible';
} catch (err) {
const catchError = err as WretchError;
status.value = 'error';
error.value = catchError?.message ? catchError : { name: 'Error', message: 'Unknown error' };
console.error('[ReplaceCheck.check]', catchError);
}
};
/**
* If we already have a validation response, set the status to eligible or ineligible
*/
onBeforeMount(() => {
if (validationResponse.value) {
status.value = validationResponse.value?.replaceable ? 'eligible' : 'ineligible';
}
});
</script>
<template>
<div class="flex flex-col">
<BrandButton
v-if="status === 'checking' || status === 'ready'"
@click="check"
:disabled="status !== 'ready'"
:icon="status === 'checking' ? BrandLoadingWhite : KeyIcon"
:text="t('Check Eligibility')"
class="w-full sm:max-w-300px"
/>
<p
v-else-if="statusOutput"
class="flex flex-row items-center gap-x-4px"
:class="[statusOutput?.classes]"
>
<component
v-if="statusOutput?.icon"
:is="statusOutput?.icon"
class="w-16px fill-current" />
{{ statusOutput?.text }}
</p>
</div>
</template>
+6 -4
View File
@@ -6,11 +6,13 @@ import { useServerStore } from '~/store/server';
export interface Props {
forExpire?: boolean;
shortText?: boolean;
t: any;
}
const props = withDefaults(defineProps<Props>(), {
forExpire: false,
shortText: false,
});
const { buildStringFromValues, dateDiff, formatDate } = useTimeHelper(props.t);
@@ -41,11 +43,11 @@ const output = computed(() => {
if (!countUp.value || state.value === 'EEXPIRED') {
return {
title: state.value === 'EEXPIRED'
? props.t('Trial Key Expired at {0}', [formattedTime.value])
: props.t('Trial Key Expires at {0}', [formattedTime.value]),
? props.t(props.shortText ? 'Expired at {0}' : 'Trial Key Expired at {0}', [formattedTime.value])
: props.t(props.shortText ? 'Expires at {0}' : 'Trial Key Expires at {0}', [formattedTime.value]),
text: state.value === 'EEXPIRED'
? props.t('Trial Key Expired {0}', [parsedTime.value])
: props.t('Trial Key Expires in {0}', [parsedTime.value]),
? props.t(props.shortText ? 'Expired {0}' : 'Trial Key Expired {0}', [parsedTime.value])
: props.t(props.shortText ? 'Expires in {0}' : 'Trial Key Expires in {0}', [parsedTime.value]),
};
}
return {
+7
View File
@@ -15,6 +15,13 @@ export const startTrial = (payload: StartTrialPayload) => KeyServer
.formUrl(payload)
.post();
export interface ValidateGuidPayload {
purchaseable: true,
registered: false,
replaceable: false,
upgradeable: false,
upgradeAllowed: 'pro' | 'plus' | 'unleashed'[],
}
export const validateGuid = (payload: { guid: string }) => KeyServer
.url('/validate/guid')
.formUrl(payload)
+7 -1
View File
@@ -251,6 +251,8 @@
"Flash Product": "Flash Product",
"Attached Storage Devices": "Attached Storage Devices",
"{0} out of {1} devices": "{0} out of {1} devices",
"{0} devices": "{0} devices",
"unlimited": "unlimited",
"Unable to check for updates": "Unable to check for updates",
"License key actions": "License key actions",
"License key type": "License key type",
@@ -260,5 +262,9 @@
"Expired {0}": "Expired {0}",
"Expires in {0}": "Expires in {0}",
"Renew your license key now": "Renew your license key now",
"Renew Key to Enable OS Updates": "Renew Key to Enable OS Updates"
"Renew Key to Enable OS Updates": "Renew Key to Enable OS Updates",
"Check Eligibility": "Check Eligibility",
"Eligible": "Eligible",
"Ineligible": "Ineligible",
"Flash GUID required": "Flash GUID required"
}
+13
View File
@@ -50,10 +50,23 @@ export const usePurchaseStore = defineStore('purchase', () => {
serverStore.inIframe,
);
};
const renew = () => {
callbackStore.send(
PURCHASE_CALLBACK.toString(),
[{
server: {
...serverStore.serverPurchasePayload,
},
type: 'renew',
}],
serverStore.inIframe,
);
};
return {
redeem,
purchase,
upgrade,
renew,
};
});
+1 -1
View File
@@ -266,7 +266,7 @@ export const useServerStore = defineStore('server', () => {
text: 'Redeem Activation Code',
};
const renewAction = ref<ServerStateDataAction>({
click: () => { purchaseStore.redeem(); },
click: () => { purchaseStore.renew(); },
external: true,
icon: KeyIcon,
name: 'renew',
+32 -1
View File
@@ -111,7 +111,38 @@ export default <Partial<Config>>{
DEFAULT: {
css: {
color: theme('colors.beta'),
'--tw-prose-body': theme('colors.beta'),
'--tw-prose-headings': theme('colors.beta'),
'--tw-prose-lead': theme('colors.beta'),
'--tw-prose-links': theme('colors.beta'),
'--tw-prose-bold': theme('colors.beta'),
'--tw-prose-counters': theme('colors.beta'),
'--tw-prose-bullets': theme('colors.beta'),
'--tw-prose-hr': theme('colors.beta'),
'--tw-prose-quotes': theme('colors.beta'),
'--tw-prose-quote-borders': theme('colors.beta'),
'--tw-prose-captions': theme('colors.beta'),
'--tw-prose-code': theme('colors.beta'),
'--tw-prose-pre-code': theme('colors.beta'),
'--tw-prose-pre-bg': theme('colors.alpha'),
'--tw-prose-th-borders': theme('colors.beta'),
'--tw-prose-td-borders': theme('colors.beta'),
'--tw-prose-invert-body': theme('colors.alpha'),
'--tw-prose-invert-headings': theme('colors.alpha'),
'--tw-prose-invert-lead': theme('colors.alpha'),
'--tw-prose-invert-links': theme('colors.alpha'),
'--tw-prose-invert-bold': theme('colors.alpha'),
'--tw-prose-invert-counters': theme('colors.alpha'),
'--tw-prose-invert-bullets': theme('colors.alpha'),
'--tw-prose-invert-hr': theme('colors.alpha'),
'--tw-prose-invert-quotes': theme('colors.alpha'),
'--tw-prose-invert-quote-borders': theme('colors.alpha'),
'--tw-prose-invert-captions': theme('colors.alpha'),
'--tw-prose-invert-code': theme('colors.alpha'),
'--tw-prose-invert-pre-code': theme('colors.alpha'),
'--tw-prose-invert-pre-bg': theme('colors.beta'),
'--tw-prose-invert-th-borders': theme('colors.alpha'),
'--tw-prose-invert-td-borders': theme('colors.alpha'),
// ...
},
},
+10
View File
@@ -0,0 +1,10 @@
import type { Component } from 'vue';
export interface RegistrationItemProps {
component?: Component;
componentProps?: object;
componentOpacity?: boolean;
error?: boolean;
label?: string;
text?: number | string | undefined;
}
+3
View File
@@ -32,6 +32,9 @@ export type ServerState = 'BASIC'
| 'EBLACKLISTED1'
| 'EBLACKLISTED2'
| 'ENOCONN'
| 'STARTER'
| 'UNLEASHED'
| 'LIFETIME'
| undefined;
export type ServerconnectPluginInstalled = 'dynamix.unraid.net.plg' | 'dynamix.unraid.net.staging.plg' | 'dynamix.unraid.net.plg_installFailed' | 'dynamix.unraid.net.staging.plg_installFailed' | '';