refactor: improved callbackfeedback and modal usage

This commit is contained in:
Zack Spear
2023-06-30 16:14:39 -07:00
parent 6b2d75dd9e
commit dc4a05916e
13 changed files with 239 additions and 207 deletions

View File

@@ -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",

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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',

View File

@@ -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>

View File

@@ -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>

View File

@@ -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',
}; };
} }
}); });

View File

@@ -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,
}; };

View File

@@ -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,

View File

@@ -97,6 +97,7 @@ export const useInstallKeyStore = defineStore('installKey', () => {
// State // State
keyInstallStatus, keyInstallStatus,
// getters // getters
keyActionType,
keyInstallStatusCopy, keyInstallStatusCopy,
keyType, keyType,
keyUrl, keyUrl,

View File

@@ -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,

View File

@@ -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;
}