mirror of
https://github.com/unraid/api.git
synced 2026-01-06 08:39:54 -06:00
refactor: improved callbackfeedback and modal usage
This commit is contained in:
@@ -51,7 +51,7 @@ const serverState = {
|
|||||||
"locale": "en_US",
|
"locale": "en_US",
|
||||||
"name": "fuji",
|
"name": "fuji",
|
||||||
// "pluginInstalled": "dynamix.unraid.net.staging.plg",
|
// "pluginInstalled": "dynamix.unraid.net.staging.plg",
|
||||||
"pluginInstalled": true,
|
"pluginInstalled": false,
|
||||||
"registered": true,
|
"registered": true,
|
||||||
"regGen": 0,
|
"regGen": 0,
|
||||||
"regGuid": "0781-5583-8355-81071A2B0211",
|
"regGuid": "0781-5583-8355-81071A2B0211",
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { XCircleIcon } from '@heroicons/vue/24/solid';
|
import { XCircleIcon } from '@heroicons/vue/24/solid';
|
||||||
export interface Props {
|
export interface Props {
|
||||||
|
btnStyle?: 'fill' | 'outline';
|
||||||
download?: boolean;
|
download?: boolean;
|
||||||
external?: boolean;
|
external?: boolean;
|
||||||
href?: string;
|
href?: string;
|
||||||
icon?: typeof XCircleIcon;
|
icon?: typeof XCircleIcon;
|
||||||
style?: 'fill' | 'outline';
|
|
||||||
text?: string;
|
text?: string;
|
||||||
}
|
}
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
style: 'fill',
|
btnStyle: 'fill',
|
||||||
});
|
});
|
||||||
|
|
||||||
defineEmits(['click']);
|
defineEmits(['click']);
|
||||||
|
|
||||||
const classes = computed(() => {
|
const classes = computed(() => {
|
||||||
switch (props.style) {
|
switch (props.btnStyle) {
|
||||||
case 'fill':
|
case 'fill':
|
||||||
return 'text-white bg-gradient-to-r from-unraid-red to-orange hover:from-unraid-red/60 hover:to-orange/60 focus:from-unraid-red/60 focus:to-orange/60';
|
return 'text-white bg-gradient-to-r from-unraid-red to-orange hover:from-unraid-red/60 hover:to-orange/60 focus:from-unraid-red/60 focus:to-orange/60';
|
||||||
case 'outline':
|
case 'outline':
|
||||||
@@ -31,14 +31,10 @@ const classes = computed(() => {
|
|||||||
:href="href"
|
:href="href"
|
||||||
:rel="external ? 'noopener noreferrer' : ''"
|
:rel="external ? 'noopener noreferrer' : ''"
|
||||||
:target="external ? '_blank' : ''"
|
:target="external ? '_blank' : ''"
|
||||||
class="text-14px text-center flex-none flex flex-row items-center justify-center gap-x-8px px-8px py-8px cursor-pointer rounded-md"
|
class="text-14px text-center font-semibold flex-none flex flex-row items-center justify-center gap-x-8px px-8px py-8px cursor-pointer rounded-md"
|
||||||
:class="classes"
|
:class="classes"
|
||||||
>
|
>
|
||||||
<component v-if="icon" :is="icon" class="flex-shrink-0 w-14px" />
|
<component v-if="icon" :is="icon" class="flex-shrink-0 w-14px" />
|
||||||
{{ text }}
|
{{ text }}
|
||||||
</component>
|
</component>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
@@ -51,7 +51,7 @@ const ariaLablledById = computed((): string|undefined => props.title ? `ModalTit
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@click="closeModal"
|
@click="closeModal"
|
||||||
class="fixed inset-0 z-0 bg-black bg-opacity-50 transition-opacity"
|
class="fixed inset-0 z-0 bg-black bg-opacity-80 transition-opacity"
|
||||||
title="Click to close modal"
|
title="Click to close modal"
|
||||||
/>
|
/>
|
||||||
</TransitionChild>
|
</TransitionChild>
|
||||||
@@ -73,18 +73,33 @@ const ariaLablledById = computed((): string|undefined => props.title ? `ModalTit
|
|||||||
success ? 'shadow-green-600/30 border-green-600/10' : '',
|
success ? 'shadow-green-600/30 border-green-600/10' : '',
|
||||||
!error && !success ? 'shadow-orange/10 border-white/10' : '',
|
!error && !success ? 'shadow-orange/10 border-white/10' : '',
|
||||||
]"
|
]"
|
||||||
class="text-alpha bg-beta border-2 border-solid relative transform overflow-hidden rounded-lg px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:p-6"
|
class="text-beta bg-alpha border-2 border-solid relative transform overflow-hidden rounded-lg px-4 pb-4 pt-5 sm:my-8 sm:p-6 text-left shadow-xl transition-all sm:w-full"
|
||||||
>
|
>
|
||||||
<div v-if="showCloseX" class="absolute z-20 right-0 top-0 hidden pt-2 pr-2 sm:block">
|
<div v-if="showCloseX" class="absolute z-20 right-0 top-0 hidden pt-2 pr-2 sm:block">
|
||||||
<button @click="closeModal" type="button" class="rounded-md text-alpha bg-beta p-2 hover:text-white focus:text-white hover:bg-unraid-red focus:bg-unraid-red focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">
|
<button @click="closeModal" type="button" class="rounded-md text-beta bg-alpha p-2 hover:text-white focus:text-white hover:bg-unraid-red focus:bg-unraid-red focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">
|
||||||
<span class="sr-only">Close</span>
|
<span class="sr-only">Close</span>
|
||||||
<XMarkIcon class="h-6 w-6" aria-hidden="true" />
|
<XMarkIcon class="h-6 w-6" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 v-if="title" :id="ariaLablledById">{{ title }}</h1>
|
<header class="text-center">
|
||||||
<h2 v-if="description">{{ description }}</h2>
|
<template v-if="!$slots['header']">
|
||||||
<slot />
|
<h1 v-if="title" :id="ariaLablledById" class="text-24px font-semibold flex flex-wrap justify-center gap-x-1">
|
||||||
|
{{ title }}
|
||||||
|
<slot name="headerTitle"></slot>
|
||||||
|
</h1>
|
||||||
|
<h2 v-if="description" class="text-16px opacity-75">{{ description }}</h2>
|
||||||
|
</template>
|
||||||
|
<slot name="header"></slot>
|
||||||
|
</header>
|
||||||
|
<slot name="main"></slot>
|
||||||
|
|
||||||
|
<footer class="text-14px relative -mx-4px -mb-4px sm:-mx-6 sm:-mb-6 p-4 sm:p-6">
|
||||||
|
<div class="absolute z-0 inset-0 opacity-10 bg-beta"></div>
|
||||||
|
<div class="relative z-10">
|
||||||
|
<slot name="footer"></slot>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</TransitionChild>
|
</TransitionChild>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useClipboard } from '@vueuse/core'
|
import { useClipboard } from '@vueuse/core'
|
||||||
import { ClipboardIcon } from '@heroicons/vue/24/solid';
|
import { ClipboardIcon, InformationCircleIcon } from '@heroicons/vue/24/solid';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import 'tailwindcss/tailwind.css';
|
import 'tailwindcss/tailwind.css';
|
||||||
import '~/assets/main.css';
|
import '~/assets/main.css';
|
||||||
import { useAccountStore } from '~/store/account';
|
import { useAccountStore } from '~/store/account';
|
||||||
import { useCallbackActionsStore } from '~/store/callbackActions';
|
import { useCallbackActionsStore } from '~/store/callbackActions';
|
||||||
import { useInstallKeyStore } from '~/store/installKey';
|
import { useInstallKeyStore } from '~/store/installKey';
|
||||||
|
import { usePromoStore } from '~/store/promo';
|
||||||
|
import { useServerStore } from '~/store/server';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
open?: boolean;
|
open?: boolean;
|
||||||
@@ -19,98 +21,131 @@ withDefaults(defineProps<Props>(), {
|
|||||||
const accountStore = useAccountStore();
|
const accountStore = useAccountStore();
|
||||||
const callbackActionsStore = useCallbackActionsStore();
|
const callbackActionsStore = useCallbackActionsStore();
|
||||||
const installKeyStore = useInstallKeyStore();
|
const installKeyStore = useInstallKeyStore();
|
||||||
|
const promoStore = usePromoStore();
|
||||||
|
const serverStore = useServerStore();
|
||||||
|
|
||||||
const { accountActionStatus, accountActionStatusCopy } = storeToRefs(accountStore);
|
const {
|
||||||
const { callbackData, callbackStatus } = storeToRefs(callbackActionsStore);
|
accountActionStatus,
|
||||||
const { keyUrl, keyInstallStatus, keyInstallStatusCopy, keyType } = storeToRefs(installKeyStore);
|
accountActionStatusCopy,
|
||||||
|
} = storeToRefs(accountStore);
|
||||||
|
const {
|
||||||
|
callbackStatus,
|
||||||
|
} = storeToRefs(callbackActionsStore);
|
||||||
|
const {
|
||||||
|
keyActionType,
|
||||||
|
keyUrl,
|
||||||
|
keyInstallStatus,
|
||||||
|
keyInstallStatusCopy,
|
||||||
|
keyType,
|
||||||
|
} = storeToRefs(installKeyStore);
|
||||||
|
const {
|
||||||
|
pluginInstalled,
|
||||||
|
} = storeToRefs(serverStore);
|
||||||
|
|
||||||
/** @todo if post purchase/upgrade thank user for their purchase and support */
|
/** @todo if post purchase/upgrade thank user for their purchase and support */
|
||||||
/** @todo if post purchase/upgrade and no Connect, show CTA to Connect promo */
|
/** @todo if post purchase/upgrade and no Connect, show CTA to Connect promo */
|
||||||
/** @todo if signing in show CTA to head to Connect settings to enable features */
|
/** @todo if signing in show CTA to head to Connect settings to enable features */
|
||||||
|
|
||||||
const heading = computed(() => callbackStatus.value === 'loading' ? 'Performing actions' : 'Finished performing actions');
|
const heading = computed(() => {
|
||||||
const subheading = computed(() => callbackStatus.value === 'loading' ? 'Please keep this window open' : '');
|
switch (callbackStatus.value) {
|
||||||
|
case 'error':
|
||||||
const modalError = computed(() => callbackStatus.value === 'done' && (keyInstallStatus.value === 'failed' || accountActionStatus.value === 'failed'));
|
return 'Error';
|
||||||
const modalSuccess = computed(() => {
|
case 'loading':
|
||||||
if (!callbackData.value) return false;
|
return 'Performing actions';
|
||||||
// if we have multiple actions, we need both to be successful
|
case 'success':
|
||||||
return callbackData.value.actions.length > 1
|
return 'Success!';
|
||||||
? callbackStatus.value === 'done' && (keyInstallStatus.value === 'success' && accountActionStatus.value === 'success')
|
}
|
||||||
: callbackStatus.value === 'done' && (keyInstallStatus.value === 'success' || accountActionStatus.value === 'success');
|
});
|
||||||
|
const subheading = computed(() => {
|
||||||
|
switch (callbackStatus.value) {
|
||||||
|
case 'loading':
|
||||||
|
return 'Please keep this window open while we perform some actions';
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// @todo keep for now as we may us`e this rather than refreshing once GQL is hooked up
|
const close = () => {
|
||||||
// const close = () => {
|
if (callbackStatus.value === 'loading') return console.debug('[close] not allowed');
|
||||||
// if (callbackStatus.value === 'loading') return console.debug('[close] not allowed');
|
window.location.reload();
|
||||||
// callbackActionsStore.setCallbackStatus('ready');
|
// callbackActionsStore.setCallbackStatus('ready');
|
||||||
// };
|
};
|
||||||
// @close="close"
|
|
||||||
// :show-close-x="!callbackStatus === 'loading'"
|
|
||||||
|
|
||||||
const reload = () => window.location.reload();
|
const promoClick = () => {
|
||||||
|
promoStore.openOnNextLoad();
|
||||||
|
close();
|
||||||
|
};
|
||||||
|
|
||||||
const { text, copy, copied, isSupported } = useClipboard({ source: keyUrl.value });
|
const { text, copy, copied, isSupported } = useClipboard({ source: keyUrl.value });
|
||||||
|
|
||||||
watch(callbackStatus, (n, o) => console.debug('[callbackStatus]', n, o));
|
|
||||||
watch(accountActionStatus, (n, o) => console.debug('[accountActionStatus]', n, o));
|
|
||||||
watch(keyInstallStatus, (n, o) => console.debug('[keyInstallStatus]', n, o));
|
|
||||||
watch(modalError, (n, o) => console.debug('[modalError]', n, o));
|
|
||||||
watch(modalSuccess, (n, o) => console.debug('[modalSuccess]', n, o));
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Modal
|
<Modal
|
||||||
|
:title="heading"
|
||||||
|
:description="subheading"
|
||||||
:open="open"
|
:open="open"
|
||||||
max-width="max-w-640px"
|
max-width="max-w-640px"
|
||||||
:error="modalError"
|
:error="callbackStatus === 'error'"
|
||||||
:success="modalSuccess"
|
:success="callbackStatus === 'success'"
|
||||||
|
@close="close"
|
||||||
|
:show-close-x="callbackStatus !== 'loading'"
|
||||||
>
|
>
|
||||||
<div class="text-16px text-center relative w-full min-h-[20vh] flex flex-col justify-between gap-y-16px">
|
<template #main>
|
||||||
<header>
|
<div class="text-16px text-center relative w-full min-h-[15vh] flex flex-col justify-center gap-y-16px">
|
||||||
<h1 class="text-24px font-semibold">{{ heading }}</h1>
|
<div
|
||||||
<p v-if="subheading" class="text-16px opacity-75">{{ subheading }}</p>
|
v-if="keyInstallStatus !== 'ready' || accountActionStatus !== 'ready'"
|
||||||
</header>
|
class="flex flex-col gap-y-16px"
|
||||||
|
>
|
||||||
<!-- <BrandLoading v-if="callbackStatus === 'loading'" class="w-90px mx-auto" /> -->
|
<UpcCallbackFeedbackStatus
|
||||||
|
v-if="keyInstallStatus !== 'ready'"
|
||||||
<UpcCallbackFeedbackStatus
|
:success="keyInstallStatus === 'success'"
|
||||||
v-if="keyInstallStatus !== 'ready'"
|
:error="keyInstallStatus === 'failed'"
|
||||||
:success="keyInstallStatus === 'success'"
|
:text="keyInstallStatusCopy.text"
|
||||||
:error="keyInstallStatus === 'failed'"
|
|
||||||
:text="keyInstallStatusCopy.text"
|
|
||||||
>
|
|
||||||
<UpcUptimeExpire v-if="keyType === 'Trial'" :for-expire="true" class="opacity-75 italic mt-4px" />
|
|
||||||
|
|
||||||
<template v-if="keyInstallStatus === 'failed'">
|
|
||||||
<div v-if="isSupported" class="flex justify-center">
|
|
||||||
<BrandButton
|
|
||||||
@click="copy(keyUrl)"
|
|
||||||
:icon="ClipboardIcon"
|
|
||||||
:text="copied ? 'Copied' : 'Copy Key URL'" />
|
|
||||||
</div>
|
|
||||||
<p v-else>Copy your Key URL: {{ keyUrl }}</p>
|
|
||||||
<p><a href="/Tools/Registration" class="opacity-75 hover:opacity-100 focus:opacity-100 underline transition">Then go to Tools > Registration to manually install it</a></p>
|
|
||||||
</template>
|
|
||||||
</UpcCallbackFeedbackStatus>
|
|
||||||
|
|
||||||
<UpcCallbackFeedbackStatus
|
|
||||||
v-if="accountActionStatus !== 'ready'"
|
|
||||||
:success="accountActionStatus === 'success'"
|
|
||||||
:error="accountActionStatus === 'failed'"
|
|
||||||
:text="accountActionStatusCopy.text" />
|
|
||||||
|
|
||||||
<footer>
|
|
||||||
<div v-if="modalSuccess" class="w-full max-w-xs flex flex-col gap-y-16px mx-auto">
|
|
||||||
<button
|
|
||||||
@click="reload"
|
|
||||||
class="opacity-75 hover:opacity-100 focus:opacity-100 underline transition"
|
|
||||||
>
|
>
|
||||||
{{ 'Reload Page to Finalize' }}
|
<UpcUptimeExpire v-if="keyType === 'Trial'" :for-expire="true" class="opacity-75 italic mt-4px" />
|
||||||
</button>
|
|
||||||
|
<template v-if="keyInstallStatus === 'failed'">
|
||||||
|
<div v-if="isSupported" class="flex justify-center">
|
||||||
|
<BrandButton
|
||||||
|
@click="copy(keyUrl)"
|
||||||
|
:icon="ClipboardIcon"
|
||||||
|
:text="copied ? 'Copied' : 'Copy Key URL'" />
|
||||||
|
</div>
|
||||||
|
<p v-else>{{ 'Copy your Key URL' }}: {{ keyUrl }}</p>
|
||||||
|
<p><a href="/Tools/Registration" class="opacity-75 hover:opacity-100 focus:opacity-100 underline transition">{{ 'Then go to Tools > Registration to manually install it' }}</a></p>
|
||||||
|
</template>
|
||||||
|
</UpcCallbackFeedbackStatus>
|
||||||
|
|
||||||
|
<UpcCallbackFeedbackStatus
|
||||||
|
v-if="accountActionStatus !== 'ready'"
|
||||||
|
:success="accountActionStatus === 'success'"
|
||||||
|
:error="accountActionStatus === 'failed'"
|
||||||
|
:text="accountActionStatusCopy.text" />
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div v-if="callbackStatus === 'success'" class="flex flex-col gap-y-16px">
|
||||||
|
<div v-if="!pluginInstalled" class="text-center flex flex-col justify-center gap-y-8px">
|
||||||
|
<p>{{ 'Enhance your Unraid experience with Unraid Connect' }}</p>
|
||||||
|
<span class="inline-flex justify-center">
|
||||||
|
<BrandButton
|
||||||
|
@click="promoClick"
|
||||||
|
:icon="InformationCircleIcon"
|
||||||
|
:text="'Learn More'"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="close"
|
||||||
|
class="opacity-75 hover:opacity-100 focus:opacity-100 underline transition"
|
||||||
|
>
|
||||||
|
{{ !pluginInstalled ? 'No Thanks' : 'Close' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</Modal>
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { storeToRefs } from 'pinia';
|
|||||||
import { ArrowRightOnRectangleIcon, ArrowTopRightOnSquareIcon, BarsArrowDownIcon, CogIcon, InformationCircleIcon, UserIcon } from '@heroicons/vue/24/solid';
|
import { ArrowRightOnRectangleIcon, ArrowTopRightOnSquareIcon, BarsArrowDownIcon, CogIcon, InformationCircleIcon, UserIcon } from '@heroicons/vue/24/solid';
|
||||||
|
|
||||||
import { ACCOUNT, CONNECT_DASHBOARD, PLUGIN_SETTINGS } from '~/helpers/urls';
|
import { ACCOUNT, CONNECT_DASHBOARD, PLUGIN_SETTINGS } from '~/helpers/urls';
|
||||||
import { useDropdownStore } from '~/store/dropdown';
|
|
||||||
import { usePromoStore } from '~/store/promo';
|
import { usePromoStore } from '~/store/promo';
|
||||||
import { useServerStore } from '~/store/server';
|
import { useServerStore } from '~/store/server';
|
||||||
import type { UserProfileLink } from '~/types/userProfile';
|
import type { UserProfileLink } from '~/types/userProfile';
|
||||||
@@ -12,7 +11,6 @@ import type { ServerStateDataAction } from '~/types/server';
|
|||||||
const myServersEnv = ref<string>('Staging');
|
const myServersEnv = ref<string>('Staging');
|
||||||
const devEnv = ref<string>('development');
|
const devEnv = ref<string>('development');
|
||||||
|
|
||||||
const dropdownStore = useDropdownStore();
|
|
||||||
const promoStore = usePromoStore();
|
const promoStore = usePromoStore();
|
||||||
const { keyActions, pluginInstalled, registered, stateData } = storeToRefs(useServerStore());
|
const { keyActions, pluginInstalled, registered, stateData } = storeToRefs(useServerStore());
|
||||||
|
|
||||||
@@ -59,7 +57,6 @@ const links = computed(():UserProfileLink[] => {
|
|||||||
{
|
{
|
||||||
click: () => {
|
click: () => {
|
||||||
promoStore.promoShow();
|
promoStore.promoShow();
|
||||||
dropdownStore.dropdownHide();
|
|
||||||
},
|
},
|
||||||
icon: InformationCircleIcon,
|
icon: InformationCircleIcon,
|
||||||
text: 'Enhance your Unraid experience with Connect',
|
text: 'Enhance your Unraid experience with Connect',
|
||||||
|
|||||||
@@ -62,32 +62,32 @@ const installButtonClasses = 'text-white text-14px text-center w-full flex flex-
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Modal
|
<Modal
|
||||||
|
title="Introducing Unraid Connect"
|
||||||
|
description="Enhance your Unraid experience"
|
||||||
:open="open"
|
:open="open"
|
||||||
@close="promoStore.promoHide()"
|
@close="promoStore.promoHide()"
|
||||||
:show-close-x="true"
|
:show-close-x="true"
|
||||||
max-width="max-w-800px"
|
max-width="max-w-800px"
|
||||||
>
|
>
|
||||||
<div class="text-center relative w-full md:p-24px">
|
<template #headerTitle>
|
||||||
<header>
|
<span><UpcBeta class="relative -top-1" /></span>
|
||||||
<h1 class="text-24px font-semibold flex flex-wrap justify-center gap-x-1">
|
</template>
|
||||||
Introducing Unraid Connect
|
|
||||||
<span><UpcBeta class="relative -top-1" /></span>
|
|
||||||
</h1>
|
|
||||||
<h2 class="text-20px">
|
|
||||||
Enhance your Unraid experience
|
|
||||||
</h2>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="flex flex-wrap justify-center my-16px md:my-24px">
|
<template #main>
|
||||||
<UpcPromoFeature
|
<div class="text-center relative w-full">
|
||||||
v-for="(feature, index) in features"
|
<div class="flex flex-wrap justify-center my-16px md:my-24px">
|
||||||
:key="index"
|
<UpcPromoFeature
|
||||||
:title="feature.title"
|
v-for="(feature, index) in features"
|
||||||
:copy="feature.copy"
|
:key="index"
|
||||||
/>
|
:title="feature.title"
|
||||||
|
:copy="feature.copy"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<div class="w-full max-w-xs flex flex-col gap-y-16px mx-auto">
|
<template #footer>
|
||||||
|
<div class="w-full max-w-xs flex flex-col items-center gap-y-16px mx-auto">
|
||||||
<!-- v-if="devEnv" -->
|
<!-- v-if="devEnv" -->
|
||||||
<SwitchGroup as="div" class="flex items-center justify-center">
|
<SwitchGroup as="div" class="flex items-center justify-center">
|
||||||
<Switch v-model="staging" :class="[staging ? 'bg-indigo-600' : 'bg-gray-200', 'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2']">
|
<Switch v-model="staging" :class="[staging ? 'bg-indigo-600' : 'bg-gray-200', 'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2']">
|
||||||
@@ -115,7 +115,7 @@ const installButtonClasses = 'text-white text-14px text-center w-full flex flex-
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
</Modal>
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -20,14 +20,14 @@ defineProps<Props>();
|
|||||||
<span v-if="!center" class="flex-shrink-0">
|
<span v-if="!center" class="flex-shrink-0">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</span>
|
</span>
|
||||||
<div class="inline-flex flex-col pl-4 mr-4" :class="{ 'text-center': center }">
|
<div class="inline-flex flex-col" :class="{ 'text-center': center }">
|
||||||
<h3 class="text-14px font-semibold" :class="{ 'mt-0 mb-4px': copy, 'my-0': !copy, 'flex flex-row justify-center items-center': center }">
|
<h3 class="text-16px font-semibold" :class="{ 'mt-0 mb-4px': copy, 'my-0': !copy, 'flex flex-row justify-center items-center': center }">
|
||||||
<span v-if="center" class="flex-shrink-0 mr-8px">
|
<span v-if="center" class="flex-shrink-0 mr-8px">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</span>
|
</span>
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</h3>
|
</h3>
|
||||||
<p v-if="copy" v-html="copy" class="text-12px opacity-90 py-0" :class="{'px-8px': center}"></p>
|
<p v-if="copy" v-html="copy" class="text-14px opacity-75 py-0" :class="{'px-8px': center}"></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -18,6 +18,8 @@ export const useAccountStore = defineStore('account', () => {
|
|||||||
const accountAction = ref<ExternalSignIn|ExternalSignOut>();
|
const accountAction = ref<ExternalSignIn|ExternalSignOut>();
|
||||||
const accountActionStatus = ref<'failed' | 'ready' | 'success' | 'updating'>('ready');
|
const accountActionStatus = ref<'failed' | 'ready' | 'success' | 'updating'>('ready');
|
||||||
|
|
||||||
|
const username = ref<string>('');
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
const recover = () => {
|
const recover = () => {
|
||||||
console.debug('[accountStore.recover]');
|
console.debug('[accountStore.recover]');
|
||||||
@@ -70,17 +72,20 @@ export const useAccountStore = defineStore('account', () => {
|
|||||||
*/
|
*/
|
||||||
const updatePluginConfig = async (action: ExternalSignIn | ExternalSignOut) => {
|
const updatePluginConfig = async (action: ExternalSignIn | ExternalSignOut) => {
|
||||||
console.debug('[accountStore.updatePluginConfig]', action);
|
console.debug('[accountStore.updatePluginConfig]', action);
|
||||||
|
// save any existing username before updating
|
||||||
|
if (serverStore.username) username.value = serverStore.username;
|
||||||
|
|
||||||
accountAction.value = action;
|
accountAction.value = action;
|
||||||
accountActionStatus.value = 'updating';
|
accountActionStatus.value = 'updating';
|
||||||
|
|
||||||
const userPayload = {
|
const userPayload = {
|
||||||
...(action.user
|
...(accountAction.value.user
|
||||||
? {
|
? {
|
||||||
apikey: action.apiKey,
|
apikey: accountAction.value.apiKey,
|
||||||
// avatar: '',
|
// avatar: '',
|
||||||
email: action.user?.email,
|
email: accountAction.value.user?.email,
|
||||||
regWizTime: `${Date.now()}_${serverStore.guid}`, // set when signing in the first time and never unset for the sake of displaying Sign In/Up in the UPC without needing to validate guid every time
|
regWizTime: `${Date.now()}_${serverStore.guid}`, // set when signing in the first time and never unset for the sake of displaying Sign In/Up in the UPC without needing to validate guid every time
|
||||||
username: action.user?.preferred_username,
|
username: accountAction.value.user?.preferred_username,
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
accesstoken: '',
|
accesstoken: '',
|
||||||
@@ -126,20 +131,20 @@ export const useAccountStore = defineStore('account', () => {
|
|||||||
case 'updating':
|
case 'updating':
|
||||||
return {
|
return {
|
||||||
text: accountAction.value?.type === 'signIn'
|
text: accountAction.value?.type === 'signIn'
|
||||||
? 'Signing in...'
|
? `Signing in ${accountAction.value.user?.preferred_username}...`
|
||||||
: 'Signing out...',
|
: `Signing out ${username.value}...`,
|
||||||
};
|
};
|
||||||
case 'success':
|
case 'success':
|
||||||
return {
|
return {
|
||||||
text: accountAction.value?.type === 'signIn'
|
text: accountAction.value?.type === 'signIn'
|
||||||
? 'Signed in successfully'
|
? `Signed ${accountAction.value.user?.preferred_username} In Successfully`
|
||||||
: 'Signed out successfully',
|
: `Signed Out ${username.value} Successfully`,
|
||||||
};
|
};
|
||||||
case 'failed':
|
case 'failed':
|
||||||
return {
|
return {
|
||||||
text: accountAction.value?.type === 'signIn'
|
text: accountAction.value?.type === 'signIn'
|
||||||
? 'Sign in failed'
|
? 'Sign In Failed'
|
||||||
: 'Sign out failed',
|
: 'Sign Out Failed',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,32 @@
|
|||||||
import AES from 'crypto-js/aes';
|
import AES from 'crypto-js/aes';
|
||||||
import Utf8 from 'crypto-js/enc-utf8';
|
import Utf8 from 'crypto-js/enc-utf8';
|
||||||
import { ref } from 'vue';
|
|
||||||
import { defineStore, createPinia, setActivePinia } from 'pinia';
|
import { defineStore, createPinia, setActivePinia } from 'pinia';
|
||||||
|
|
||||||
export interface ServerAccountCallbackServerData {
|
/**
|
||||||
|
* @see https://stackoverflow.com/questions/73476371/using-pinia-with-vue-js-web-components
|
||||||
|
* @see https://github.com/vuejs/pinia/discussions/1085
|
||||||
|
*/
|
||||||
|
setActivePinia(createPinia());
|
||||||
|
|
||||||
|
export type SignIn = 'signIn';
|
||||||
|
export type SignOut = 'signOut';
|
||||||
|
export type OemSignOut = 'oemSignOut';
|
||||||
|
export type Troubleshoot = 'troubleshoot';
|
||||||
|
export type Recover = 'recover';
|
||||||
|
export type Replace = 'replace';
|
||||||
|
export type TrialExtend = 'trialExtend';
|
||||||
|
export type TrialStart = 'trialStart';
|
||||||
|
export type Purchase = 'purchase';
|
||||||
|
export type Redeem = 'redeem';
|
||||||
|
export type Upgrade = 'upgrade';
|
||||||
|
|
||||||
|
export type AccountAction = SignIn | SignOut | OemSignOut | Troubleshoot;
|
||||||
|
export type AccountKeyAction = Recover | Replace | TrialExtend | TrialStart;
|
||||||
|
export type PurchaseAction = Purchase | Redeem | Upgrade;
|
||||||
|
|
||||||
|
export type ServerAction = AccountAction | AccountKeyAction | PurchaseAction;
|
||||||
|
|
||||||
|
export interface ServerData {
|
||||||
description?: string;
|
description?: string;
|
||||||
deviceCount?: number;
|
deviceCount?: number;
|
||||||
expireTime?: number;
|
expireTime?: number;
|
||||||
@@ -20,26 +43,9 @@ export interface ServerAccountCallbackServerData {
|
|||||||
wanFQDN?: string;
|
wanFQDN?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SignIn = 'signIn';
|
|
||||||
export type SignOut = 'signOut';
|
|
||||||
export type Troubleshoot = 'troubleshoot';
|
|
||||||
export type Recover = 'recover';
|
|
||||||
export type Replace = 'replace';
|
|
||||||
export type TrialExtend = 'trialExtend';
|
|
||||||
export type TrialStart = 'trialStart';
|
|
||||||
export type Purchase = 'purchase';
|
|
||||||
export type Redeem = 'redeem';
|
|
||||||
export type Upgrade = 'upgrade';
|
|
||||||
|
|
||||||
export type AccountAction = SignIn | SignOut | Troubleshoot;
|
|
||||||
export type AccountKeyAction = Recover | Replace | TrialExtend | TrialStart;
|
|
||||||
export type PurchaseAction = Purchase | Redeem | Upgrade;
|
|
||||||
|
|
||||||
export type ServerStateDataActionType = AccountAction | AccountKeyAction | PurchaseAction;
|
|
||||||
|
|
||||||
export interface ServerPayload {
|
export interface ServerPayload {
|
||||||
server: ServerAccountCallbackServerData;
|
server: ServerData;
|
||||||
type: ServerStateDataActionType;
|
type: ServerAction;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExternalSignIn {
|
export interface ExternalSignIn {
|
||||||
@@ -48,7 +54,7 @@ export interface ExternalSignIn {
|
|||||||
user: UserInfo;
|
user: UserInfo;
|
||||||
}
|
}
|
||||||
export interface ExternalSignOut {
|
export interface ExternalSignOut {
|
||||||
type: SignOut;
|
type: SignOut | OemSignOut;
|
||||||
}
|
}
|
||||||
export interface ExternalKeyActions {
|
export interface ExternalKeyActions {
|
||||||
type: PurchaseAction | AccountKeyAction;
|
type: PurchaseAction | AccountKeyAction;
|
||||||
@@ -92,12 +98,6 @@ interface CallbackActionsStore {
|
|||||||
sendType: 'fromUpc' | 'forUpc';
|
sendType: 'fromUpc' | 'forUpc';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @see https://stackoverflow.com/questions/73476371/using-pinia-with-vue-js-web-components
|
|
||||||
* @see https://github.com/vuejs/pinia/discussions/1085
|
|
||||||
*/
|
|
||||||
setActivePinia(createPinia());
|
|
||||||
|
|
||||||
export const useCallbackStoreGeneric = (
|
export const useCallbackStoreGeneric = (
|
||||||
useCallbackActions: () => CallbackActionsStore,
|
useCallbackActions: () => CallbackActionsStore,
|
||||||
) =>
|
) =>
|
||||||
@@ -105,10 +105,7 @@ export const useCallbackStoreGeneric = (
|
|||||||
const callbackActions = useCallbackActions();
|
const callbackActions = useCallbackActions();
|
||||||
const encryptionKey = 'Uyv2o8e*FiQe8VeLekTqyX6Z*8XonB';
|
const encryptionKey = 'Uyv2o8e*FiQe8VeLekTqyX6Z*8XonB';
|
||||||
const sendType = 'fromUpc';
|
const sendType = 'fromUpc';
|
||||||
// state
|
|
||||||
const encryptedMessage = ref<string | null>(null);
|
|
||||||
|
|
||||||
// actions
|
|
||||||
const send = (url: string, payload: SendPayloads) => {
|
const send = (url: string, payload: SendPayloads) => {
|
||||||
console.debug('[callback.send]');
|
console.debug('[callback.send]');
|
||||||
const stringifiedData = JSON.stringify({
|
const stringifiedData = JSON.stringify({
|
||||||
@@ -118,11 +115,11 @@ export const useCallbackStoreGeneric = (
|
|||||||
sender: window.location.href,
|
sender: window.location.href,
|
||||||
type: sendType,
|
type: sendType,
|
||||||
});
|
});
|
||||||
encryptedMessage.value = AES.encrypt(stringifiedData, encryptionKey).toString();
|
const encryptedMessage = AES.encrypt(stringifiedData, encryptionKey).toString();
|
||||||
// build and go to url
|
// build and go to url
|
||||||
const destinationUrl = new URL(url);
|
const destinationUrl = new URL(url);
|
||||||
destinationUrl.searchParams.set('data', encodeURI(encryptedMessage.value));
|
destinationUrl.searchParams.set('data', encodeURI(encryptedMessage));
|
||||||
console.debug('[callback.send]', encryptedMessage.value, destinationUrl);
|
console.debug('[callback.send]', encryptedMessage, destinationUrl);
|
||||||
window.location.href = destinationUrl.toString();
|
window.location.href = destinationUrl.toString();
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -144,7 +141,6 @@ export const useCallbackStoreGeneric = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// actions
|
|
||||||
send,
|
send,
|
||||||
watcher,
|
watcher,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { defineStore } from 'pinia';
|
|||||||
|
|
||||||
import { useAccountStore } from './account';
|
import { useAccountStore } from './account';
|
||||||
import { useInstallKeyStore } from './installKey';
|
import { useInstallKeyStore } from './installKey';
|
||||||
import { useCallbackStoreGeneric, type ExternalPayload, type ExternalActions, type ExternalKeyActions, type QueryPayloads } from './callback';
|
import { useCallbackStoreGeneric, type ExternalPayload, type ExternalKeyActions, type QueryPayloads } from './callback';
|
||||||
// import { useServerStore } from './server';
|
// import { useServerStore } from './server';
|
||||||
|
|
||||||
export const useCallbackActionsStore = defineStore(
|
export const useCallbackActionsStore = defineStore(
|
||||||
@@ -11,7 +11,7 @@ export const useCallbackActionsStore = defineStore(
|
|||||||
const accountStore = useAccountStore();
|
const accountStore = useAccountStore();
|
||||||
const installKeyStore = useInstallKeyStore();
|
const installKeyStore = useInstallKeyStore();
|
||||||
// const serverStore = useServerStore();
|
// const serverStore = useServerStore();
|
||||||
type CallbackStatus = 'error' | 'loading' | 'ready' | 'done';
|
type CallbackStatus = 'error' | 'loading' | 'ready' | 'success';
|
||||||
const callbackStatus = ref<CallbackStatus>('ready');
|
const callbackStatus = ref<CallbackStatus>('ready');
|
||||||
|
|
||||||
const callbackData = ref<ExternalPayload>();
|
const callbackData = ref<ExternalPayload>();
|
||||||
@@ -38,22 +38,21 @@ export const useCallbackActionsStore = defineStore(
|
|||||||
if (action?.keyUrl) {
|
if (action?.keyUrl) {
|
||||||
await installKeyStore.install(action as ExternalKeyActions);
|
await installKeyStore.install(action as ExternalKeyActions);
|
||||||
}
|
}
|
||||||
/** @todo add oemSignOut */
|
if (action?.user || action.type === 'signOut' || action.type === 'oemSignOut') {
|
||||||
if (action?.user || action.type === 'signOut') {
|
|
||||||
await accountStore.updatePluginConfig(action);
|
await accountStore.updatePluginConfig(action);
|
||||||
}
|
}
|
||||||
// all actions have run
|
// all actions have run
|
||||||
if (array.length === (index + 1)) {
|
if (array.length === (index + 1)) {
|
||||||
callbackStatus.value = 'done';
|
// callbackStatus.value = 'done';
|
||||||
// if (array.length > 1) {
|
if (array.length > 1) {
|
||||||
// // if we have more than 1 action it means there was a key install and an account action so both need to be successful
|
// if we have more than 1 action it means there was a key install and an account action so both need to be successful
|
||||||
// const allSuccess = accountStore.accountActionStatus === 'success' && installKeyStore.keyInstallStatus === 'success';
|
const allSuccess = accountStore.accountActionStatus === 'success' && installKeyStore.keyInstallStatus === 'success';
|
||||||
// callbackStatus.value = allSuccess ? 'success' : 'error';
|
callbackStatus.value = allSuccess ? 'success' : 'error';
|
||||||
// } else {
|
} else {
|
||||||
// // only 1 action needs to be successful
|
// only 1 action needs to be successful
|
||||||
// const oneSuccess = accountStore.accountActionStatus === 'success' || installKeyStore.keyInstallStatus === 'success';
|
const oneSuccess = accountStore.accountActionStatus === 'success' || installKeyStore.keyInstallStatus === 'success';
|
||||||
// callbackStatus.value = oneSuccess ? 'success' : 'error';
|
callbackStatus.value = oneSuccess ? 'success' : 'error';
|
||||||
// }
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -83,6 +82,10 @@ export const useCallbackActionsStore = defineStore(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(callbackData, () => {
|
||||||
|
console.debug('[callbackData] watch', callbackData.value);
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
redirectToCallbackType,
|
redirectToCallbackType,
|
||||||
callbackData,
|
callbackData,
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ export const useInstallKeyStore = defineStore('installKey', () => {
|
|||||||
// State
|
// State
|
||||||
keyInstallStatus,
|
keyInstallStatus,
|
||||||
// getters
|
// getters
|
||||||
|
keyActionType,
|
||||||
keyInstallStatusCopy,
|
keyInstallStatusCopy,
|
||||||
keyType,
|
keyType,
|
||||||
keyUrl,
|
keyUrl,
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { useToggle } from '@vueuse/core';
|
import { useToggle } from '@vueuse/core';
|
||||||
import { defineStore, createPinia, setActivePinia } from 'pinia';
|
import { defineStore, createPinia, setActivePinia } from 'pinia';
|
||||||
|
|
||||||
|
import { useDropdownStore } from '~/store/dropdown';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see https://stackoverflow.com/questions/73476371/using-pinia-with-vue-js-web-components
|
* @see https://stackoverflow.com/questions/73476371/using-pinia-with-vue-js-web-components
|
||||||
* @see https://github.com/vuejs/pinia/discussions/1085
|
* @see https://github.com/vuejs/pinia/discussions/1085
|
||||||
@@ -8,17 +10,33 @@ import { defineStore, createPinia, setActivePinia } from 'pinia';
|
|||||||
setActivePinia(createPinia());
|
setActivePinia(createPinia());
|
||||||
|
|
||||||
export const usePromoStore = defineStore('promo', () => {
|
export const usePromoStore = defineStore('promo', () => {
|
||||||
const promoVisible = ref<boolean>(false);
|
const dropdownStore = useDropdownStore();
|
||||||
|
|
||||||
|
const promoVisible = ref<boolean>(false);
|
||||||
|
|
||||||
|
const openOnNextLoad = () => sessionStorage.setItem('unraidConnectPromo', 'show');
|
||||||
const promoHide = () => promoVisible.value = false;
|
const promoHide = () => promoVisible.value = false;
|
||||||
const promoShow = () => promoVisible.value = true;
|
const promoShow = () => promoVisible.value = true;
|
||||||
const promoToggle = useToggle(promoVisible);
|
const promoToggle = useToggle(promoVisible);
|
||||||
|
|
||||||
watch(promoVisible, (newVal, _oldVal) => {
|
watch(promoVisible, (newVal, _oldVal) => {
|
||||||
console.debug('[promoVisible]', newVal, _oldVal);
|
console.debug('[promoVisible]', newVal, _oldVal);
|
||||||
|
if (newVal) { // close the dropdown when the promo is opened
|
||||||
|
dropdownStore.dropdownHide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeMount(() => {
|
||||||
|
if (sessionStorage.getItem('unraidConnectPromo') === 'show') {
|
||||||
|
sessionStorage.removeItem('unraidConnectPromo');
|
||||||
|
promoShow();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
promoVisible,
|
promoVisible,
|
||||||
|
|
||||||
|
openOnNextLoad,
|
||||||
promoHide,
|
promoHide,
|
||||||
promoShow,
|
promoShow,
|
||||||
promoToggle,
|
promoToggle,
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
import type { CognitoUser, ChallengeName } from 'amazon-cognito-identity-js';
|
|
||||||
import type {
|
|
||||||
ServerAccountCallbackSendPayload,
|
|
||||||
ServerPurchaseCallbackSendPayload,
|
|
||||||
ServerStateDataActionType,
|
|
||||||
} from '~/types/server';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* These user interfaces are mimiced from the Auth repo
|
|
||||||
*/
|
|
||||||
export interface UserInfo {
|
|
||||||
'custom:ips_id'?: string;
|
|
||||||
email?: string;
|
|
||||||
email_verifed?: 'true' | 'false';
|
|
||||||
preferred_username?: string;
|
|
||||||
sub?: string;
|
|
||||||
username?: string;
|
|
||||||
}
|
|
||||||
export interface CallbackSendPayload {
|
|
||||||
server: ServerAccountCallbackSendPayload|ServerPurchaseCallbackSendPayload;
|
|
||||||
type: ServerStateDataActionType;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CallbackAction {
|
|
||||||
apiKey?: string;
|
|
||||||
keyUrl?: string;
|
|
||||||
type: ServerStateDataActionType;
|
|
||||||
user?: UserInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CallbackReceivePayload {
|
|
||||||
actions: CallbackAction[];
|
|
||||||
sender: string;
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user