mirror of
https://github.com/unraid/api.git
synced 2026-01-02 14:40:01 -06:00
refactor: improved callbackfeedback and modal usage
This commit is contained in:
@@ -51,7 +51,7 @@ const serverState = {
|
||||
"locale": "en_US",
|
||||
"name": "fuji",
|
||||
// "pluginInstalled": "dynamix.unraid.net.staging.plg",
|
||||
"pluginInstalled": true,
|
||||
"pluginInstalled": false,
|
||||
"registered": true,
|
||||
"regGen": 0,
|
||||
"regGuid": "0781-5583-8355-81071A2B0211",
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import { XCircleIcon } from '@heroicons/vue/24/solid';
|
||||
export interface Props {
|
||||
btnStyle?: 'fill' | 'outline';
|
||||
download?: boolean;
|
||||
external?: boolean;
|
||||
href?: string;
|
||||
icon?: typeof XCircleIcon;
|
||||
style?: 'fill' | 'outline';
|
||||
text?: string;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
style: 'fill',
|
||||
btnStyle: 'fill',
|
||||
});
|
||||
|
||||
defineEmits(['click']);
|
||||
|
||||
const classes = computed(() => {
|
||||
switch (props.style) {
|
||||
switch (props.btnStyle) {
|
||||
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';
|
||||
case 'outline':
|
||||
@@ -31,14 +31,10 @@ const classes = computed(() => {
|
||||
:href="href"
|
||||
:rel="external ? 'noopener noreferrer' : ''"
|
||||
: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"
|
||||
>
|
||||
<component v-if="icon" :is="icon" class="flex-shrink-0 w-14px" />
|
||||
{{ text }}
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -51,7 +51,7 @@ const ariaLablledById = computed((): string|undefined => props.title ? `ModalTit
|
||||
>
|
||||
<div
|
||||
@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"
|
||||
/>
|
||||
</TransitionChild>
|
||||
@@ -73,18 +73,33 @@ const ariaLablledById = computed((): string|undefined => props.title ? `ModalTit
|
||||
success ? 'shadow-green-600/30 border-green-600/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">
|
||||
<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>
|
||||
<XMarkIcon class="h-6 w-6" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<h1 v-if="title" :id="ariaLablledById">{{ title }}</h1>
|
||||
<h2 v-if="description">{{ description }}</h2>
|
||||
<slot />
|
||||
<header class="text-center">
|
||||
<template v-if="!$slots['header']">
|
||||
<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>
|
||||
</TransitionChild>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
<script lang="ts" setup>
|
||||
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 'tailwindcss/tailwind.css';
|
||||
import '~/assets/main.css';
|
||||
import { useAccountStore } from '~/store/account';
|
||||
import { useCallbackActionsStore } from '~/store/callbackActions';
|
||||
import { useInstallKeyStore } from '~/store/installKey';
|
||||
import { usePromoStore } from '~/store/promo';
|
||||
import { useServerStore } from '~/store/server';
|
||||
|
||||
export interface Props {
|
||||
open?: boolean;
|
||||
@@ -19,98 +21,131 @@ withDefaults(defineProps<Props>(), {
|
||||
const accountStore = useAccountStore();
|
||||
const callbackActionsStore = useCallbackActionsStore();
|
||||
const installKeyStore = useInstallKeyStore();
|
||||
const promoStore = usePromoStore();
|
||||
const serverStore = useServerStore();
|
||||
|
||||
const { accountActionStatus, accountActionStatusCopy } = storeToRefs(accountStore);
|
||||
const { callbackData, callbackStatus } = storeToRefs(callbackActionsStore);
|
||||
const { keyUrl, keyInstallStatus, keyInstallStatusCopy, keyType } = storeToRefs(installKeyStore);
|
||||
const {
|
||||
accountActionStatus,
|
||||
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 and no Connect, show CTA to Connect promo */
|
||||
/** @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 subheading = computed(() => callbackStatus.value === 'loading' ? 'Please keep this window open' : '');
|
||||
|
||||
const modalError = computed(() => callbackStatus.value === 'done' && (keyInstallStatus.value === 'failed' || accountActionStatus.value === 'failed'));
|
||||
const modalSuccess = computed(() => {
|
||||
if (!callbackData.value) return false;
|
||||
// if we have multiple actions, we need both to be successful
|
||||
return callbackData.value.actions.length > 1
|
||||
? callbackStatus.value === 'done' && (keyInstallStatus.value === 'success' && accountActionStatus.value === 'success')
|
||||
: callbackStatus.value === 'done' && (keyInstallStatus.value === 'success' || accountActionStatus.value === 'success');
|
||||
const heading = computed(() => {
|
||||
switch (callbackStatus.value) {
|
||||
case 'error':
|
||||
return 'Error';
|
||||
case 'loading':
|
||||
return 'Performing actions';
|
||||
case 'success':
|
||||
return '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 = () => {
|
||||
// if (callbackStatus.value === 'loading') return console.debug('[close] not allowed');
|
||||
// callbackActionsStore.setCallbackStatus('ready');
|
||||
// };
|
||||
// @close="close"
|
||||
// :show-close-x="!callbackStatus === 'loading'"
|
||||
const close = () => {
|
||||
if (callbackStatus.value === 'loading') return console.debug('[close] not allowed');
|
||||
window.location.reload();
|
||||
// callbackActionsStore.setCallbackStatus('ready');
|
||||
};
|
||||
|
||||
const reload = () => window.location.reload();
|
||||
const promoClick = () => {
|
||||
promoStore.openOnNextLoad();
|
||||
close();
|
||||
};
|
||||
|
||||
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>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
:title="heading"
|
||||
:description="subheading"
|
||||
:open="open"
|
||||
max-width="max-w-640px"
|
||||
:error="modalError"
|
||||
:success="modalSuccess"
|
||||
:error="callbackStatus === 'error'"
|
||||
: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">
|
||||
<header>
|
||||
<h1 class="text-24px font-semibold">{{ heading }}</h1>
|
||||
<p v-if="subheading" class="text-16px opacity-75">{{ subheading }}</p>
|
||||
</header>
|
||||
|
||||
<!-- <BrandLoading v-if="callbackStatus === 'loading'" class="w-90px mx-auto" /> -->
|
||||
|
||||
<UpcCallbackFeedbackStatus
|
||||
v-if="keyInstallStatus !== 'ready'"
|
||||
:success="keyInstallStatus === 'success'"
|
||||
: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"
|
||||
<template #main>
|
||||
<div class="text-16px text-center relative w-full min-h-[15vh] flex flex-col justify-center gap-y-16px">
|
||||
<div
|
||||
v-if="keyInstallStatus !== 'ready' || accountActionStatus !== 'ready'"
|
||||
class="flex flex-col gap-y-16px"
|
||||
>
|
||||
<UpcCallbackFeedbackStatus
|
||||
v-if="keyInstallStatus !== 'ready'"
|
||||
:success="keyInstallStatus === 'success'"
|
||||
:error="keyInstallStatus === 'failed'"
|
||||
:text="keyInstallStatusCopy.text"
|
||||
>
|
||||
{{ 'Reload Page to Finalize' }}
|
||||
</button>
|
||||
<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" />
|
||||
</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>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import { storeToRefs } from 'pinia';
|
||||
import { ArrowRightOnRectangleIcon, ArrowTopRightOnSquareIcon, BarsArrowDownIcon, CogIcon, InformationCircleIcon, UserIcon } from '@heroicons/vue/24/solid';
|
||||
|
||||
import { ACCOUNT, CONNECT_DASHBOARD, PLUGIN_SETTINGS } from '~/helpers/urls';
|
||||
import { useDropdownStore } from '~/store/dropdown';
|
||||
import { usePromoStore } from '~/store/promo';
|
||||
import { useServerStore } from '~/store/server';
|
||||
import type { UserProfileLink } from '~/types/userProfile';
|
||||
@@ -12,7 +11,6 @@ import type { ServerStateDataAction } from '~/types/server';
|
||||
const myServersEnv = ref<string>('Staging');
|
||||
const devEnv = ref<string>('development');
|
||||
|
||||
const dropdownStore = useDropdownStore();
|
||||
const promoStore = usePromoStore();
|
||||
const { keyActions, pluginInstalled, registered, stateData } = storeToRefs(useServerStore());
|
||||
|
||||
@@ -59,7 +57,6 @@ const links = computed(():UserProfileLink[] => {
|
||||
{
|
||||
click: () => {
|
||||
promoStore.promoShow();
|
||||
dropdownStore.dropdownHide();
|
||||
},
|
||||
icon: InformationCircleIcon,
|
||||
text: 'Enhance your Unraid experience with Connect',
|
||||
|
||||
@@ -62,32 +62,32 @@ const installButtonClasses = 'text-white text-14px text-center w-full flex flex-
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
title="Introducing Unraid Connect"
|
||||
description="Enhance your Unraid experience"
|
||||
:open="open"
|
||||
@close="promoStore.promoHide()"
|
||||
:show-close-x="true"
|
||||
max-width="max-w-800px"
|
||||
>
|
||||
<div class="text-center relative w-full md:p-24px">
|
||||
<header>
|
||||
<h1 class="text-24px font-semibold flex flex-wrap justify-center gap-x-1">
|
||||
Introducing Unraid Connect
|
||||
<span><UpcBeta class="relative -top-1" /></span>
|
||||
</h1>
|
||||
<h2 class="text-20px">
|
||||
Enhance your Unraid experience
|
||||
</h2>
|
||||
</header>
|
||||
<template #headerTitle>
|
||||
<span><UpcBeta class="relative -top-1" /></span>
|
||||
</template>
|
||||
|
||||
<div class="flex flex-wrap justify-center my-16px md:my-24px">
|
||||
<UpcPromoFeature
|
||||
v-for="(feature, index) in features"
|
||||
:key="index"
|
||||
:title="feature.title"
|
||||
:copy="feature.copy"
|
||||
/>
|
||||
<template #main>
|
||||
<div class="text-center relative w-full">
|
||||
<div class="flex flex-wrap justify-center my-16px md:my-24px">
|
||||
<UpcPromoFeature
|
||||
v-for="(feature, index) in features"
|
||||
:key="index"
|
||||
:title="feature.title"
|
||||
:copy="feature.copy"
|
||||
/>
|
||||
</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" -->
|
||||
<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']">
|
||||
@@ -115,7 +115,7 @@ const installButtonClasses = 'text-white text-14px text-center w-full flex flex-
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -20,14 +20,14 @@ defineProps<Props>();
|
||||
<span v-if="!center" class="flex-shrink-0">
|
||||
<slot></slot>
|
||||
</span>
|
||||
<div class="inline-flex flex-col pl-4 mr-4" :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 }">
|
||||
<div class="inline-flex flex-col" :class="{ 'text-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">
|
||||
<slot></slot>
|
||||
</span>
|
||||
{{ title }}
|
||||
</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>
|
||||
</template>
|
||||
@@ -18,6 +18,8 @@ export const useAccountStore = defineStore('account', () => {
|
||||
const accountAction = ref<ExternalSignIn|ExternalSignOut>();
|
||||
const accountActionStatus = ref<'failed' | 'ready' | 'success' | 'updating'>('ready');
|
||||
|
||||
const username = ref<string>('');
|
||||
|
||||
// Actions
|
||||
const recover = () => {
|
||||
console.debug('[accountStore.recover]');
|
||||
@@ -70,17 +72,20 @@ export const useAccountStore = defineStore('account', () => {
|
||||
*/
|
||||
const updatePluginConfig = async (action: ExternalSignIn | ExternalSignOut) => {
|
||||
console.debug('[accountStore.updatePluginConfig]', action);
|
||||
// save any existing username before updating
|
||||
if (serverStore.username) username.value = serverStore.username;
|
||||
|
||||
accountAction.value = action;
|
||||
accountActionStatus.value = 'updating';
|
||||
|
||||
const userPayload = {
|
||||
...(action.user
|
||||
...(accountAction.value.user
|
||||
? {
|
||||
apikey: action.apiKey,
|
||||
apikey: accountAction.value.apiKey,
|
||||
// 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
|
||||
username: action.user?.preferred_username,
|
||||
username: accountAction.value.user?.preferred_username,
|
||||
}
|
||||
: {
|
||||
accesstoken: '',
|
||||
@@ -126,20 +131,20 @@ export const useAccountStore = defineStore('account', () => {
|
||||
case 'updating':
|
||||
return {
|
||||
text: accountAction.value?.type === 'signIn'
|
||||
? 'Signing in...'
|
||||
: 'Signing out...',
|
||||
? `Signing in ${accountAction.value.user?.preferred_username}...`
|
||||
: `Signing out ${username.value}...`,
|
||||
};
|
||||
case 'success':
|
||||
return {
|
||||
text: accountAction.value?.type === 'signIn'
|
||||
? 'Signed in successfully'
|
||||
: 'Signed out successfully',
|
||||
? `Signed ${accountAction.value.user?.preferred_username} In Successfully`
|
||||
: `Signed Out ${username.value} Successfully`,
|
||||
};
|
||||
case 'failed':
|
||||
return {
|
||||
text: accountAction.value?.type === 'signIn'
|
||||
? 'Sign in failed'
|
||||
: 'Sign out failed',
|
||||
? 'Sign In Failed'
|
||||
: 'Sign Out Failed',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,9 +1,32 @@
|
||||
import AES from 'crypto-js/aes';
|
||||
import Utf8 from 'crypto-js/enc-utf8';
|
||||
import { ref } from 'vue';
|
||||
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;
|
||||
deviceCount?: number;
|
||||
expireTime?: number;
|
||||
@@ -20,26 +43,9 @@ export interface ServerAccountCallbackServerData {
|
||||
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 {
|
||||
server: ServerAccountCallbackServerData;
|
||||
type: ServerStateDataActionType;
|
||||
server: ServerData;
|
||||
type: ServerAction;
|
||||
}
|
||||
|
||||
export interface ExternalSignIn {
|
||||
@@ -48,7 +54,7 @@ export interface ExternalSignIn {
|
||||
user: UserInfo;
|
||||
}
|
||||
export interface ExternalSignOut {
|
||||
type: SignOut;
|
||||
type: SignOut | OemSignOut;
|
||||
}
|
||||
export interface ExternalKeyActions {
|
||||
type: PurchaseAction | AccountKeyAction;
|
||||
@@ -92,12 +98,6 @@ interface CallbackActionsStore {
|
||||
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 = (
|
||||
useCallbackActions: () => CallbackActionsStore,
|
||||
) =>
|
||||
@@ -105,10 +105,7 @@ export const useCallbackStoreGeneric = (
|
||||
const callbackActions = useCallbackActions();
|
||||
const encryptionKey = 'Uyv2o8e*FiQe8VeLekTqyX6Z*8XonB';
|
||||
const sendType = 'fromUpc';
|
||||
// state
|
||||
const encryptedMessage = ref<string | null>(null);
|
||||
|
||||
// actions
|
||||
const send = (url: string, payload: SendPayloads) => {
|
||||
console.debug('[callback.send]');
|
||||
const stringifiedData = JSON.stringify({
|
||||
@@ -118,11 +115,11 @@ export const useCallbackStoreGeneric = (
|
||||
sender: window.location.href,
|
||||
type: sendType,
|
||||
});
|
||||
encryptedMessage.value = AES.encrypt(stringifiedData, encryptionKey).toString();
|
||||
const encryptedMessage = AES.encrypt(stringifiedData, encryptionKey).toString();
|
||||
// build and go to url
|
||||
const destinationUrl = new URL(url);
|
||||
destinationUrl.searchParams.set('data', encodeURI(encryptedMessage.value));
|
||||
console.debug('[callback.send]', encryptedMessage.value, destinationUrl);
|
||||
destinationUrl.searchParams.set('data', encodeURI(encryptedMessage));
|
||||
console.debug('[callback.send]', encryptedMessage, destinationUrl);
|
||||
window.location.href = destinationUrl.toString();
|
||||
return;
|
||||
};
|
||||
@@ -144,7 +141,6 @@ export const useCallbackStoreGeneric = (
|
||||
};
|
||||
|
||||
return {
|
||||
// actions
|
||||
send,
|
||||
watcher,
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import { defineStore } from 'pinia';
|
||||
|
||||
import { useAccountStore } from './account';
|
||||
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';
|
||||
|
||||
export const useCallbackActionsStore = defineStore(
|
||||
@@ -11,7 +11,7 @@ export const useCallbackActionsStore = defineStore(
|
||||
const accountStore = useAccountStore();
|
||||
const installKeyStore = useInstallKeyStore();
|
||||
// const serverStore = useServerStore();
|
||||
type CallbackStatus = 'error' | 'loading' | 'ready' | 'done';
|
||||
type CallbackStatus = 'error' | 'loading' | 'ready' | 'success';
|
||||
const callbackStatus = ref<CallbackStatus>('ready');
|
||||
|
||||
const callbackData = ref<ExternalPayload>();
|
||||
@@ -38,22 +38,21 @@ export const useCallbackActionsStore = defineStore(
|
||||
if (action?.keyUrl) {
|
||||
await installKeyStore.install(action as ExternalKeyActions);
|
||||
}
|
||||
/** @todo add oemSignOut */
|
||||
if (action?.user || action.type === 'signOut') {
|
||||
if (action?.user || action.type === 'signOut' || action.type === 'oemSignOut') {
|
||||
await accountStore.updatePluginConfig(action);
|
||||
}
|
||||
// all actions have run
|
||||
if (array.length === (index + 1)) {
|
||||
callbackStatus.value = 'done';
|
||||
// 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
|
||||
// const allSuccess = accountStore.accountActionStatus === 'success' && installKeyStore.keyInstallStatus === 'success';
|
||||
// callbackStatus.value = allSuccess ? 'success' : 'error';
|
||||
// } else {
|
||||
// // only 1 action needs to be successful
|
||||
// const oneSuccess = accountStore.accountActionStatus === 'success' || installKeyStore.keyInstallStatus === 'success';
|
||||
// callbackStatus.value = oneSuccess ? 'success' : 'error';
|
||||
// }
|
||||
// callbackStatus.value = 'done';
|
||||
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
|
||||
const allSuccess = accountStore.accountActionStatus === 'success' && installKeyStore.keyInstallStatus === 'success';
|
||||
callbackStatus.value = allSuccess ? 'success' : 'error';
|
||||
} else {
|
||||
// only 1 action needs to be successful
|
||||
const oneSuccess = accountStore.accountActionStatus === 'success' || installKeyStore.keyInstallStatus === 'success';
|
||||
callbackStatus.value = oneSuccess ? 'success' : 'error';
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -83,6 +82,10 @@ export const useCallbackActionsStore = defineStore(
|
||||
}
|
||||
});
|
||||
|
||||
watch(callbackData, () => {
|
||||
console.debug('[callbackData] watch', callbackData.value);
|
||||
});
|
||||
|
||||
return {
|
||||
redirectToCallbackType,
|
||||
callbackData,
|
||||
|
||||
@@ -97,6 +97,7 @@ export const useInstallKeyStore = defineStore('installKey', () => {
|
||||
// State
|
||||
keyInstallStatus,
|
||||
// getters
|
||||
keyActionType,
|
||||
keyInstallStatusCopy,
|
||||
keyType,
|
||||
keyUrl,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useToggle } from '@vueuse/core';
|
||||
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://github.com/vuejs/pinia/discussions/1085
|
||||
@@ -8,17 +10,33 @@ import { defineStore, createPinia, setActivePinia } from 'pinia';
|
||||
setActivePinia(createPinia());
|
||||
|
||||
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 promoShow = () => promoVisible.value = true;
|
||||
const promoToggle = useToggle(promoVisible);
|
||||
|
||||
watch(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 {
|
||||
promoVisible,
|
||||
|
||||
openOnNextLoad,
|
||||
promoHide,
|
||||
promoShow,
|
||||
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