mirror of
https://github.com/unraid/api.git
synced 2026-01-07 00:59:48 -06:00
feat: start trial from upc
This commit is contained in:
@@ -29,7 +29,7 @@ const blacklistedGuid = '154B-00EE-0700-9B50CF819816';
|
||||
// EBLACKLISTED1
|
||||
// EBLACKLISTED2
|
||||
// ENOCONN
|
||||
const state: string = 'PLUS';
|
||||
const state: string = 'ENOKEYFILE';
|
||||
|
||||
const uptime = Date.now() - 60 * 60 * 1000; // 1 hour ago
|
||||
let expireTime = 0;
|
||||
@@ -44,7 +44,8 @@ const serverState = {
|
||||
expireTime,
|
||||
"flashProduct": "SanDisk_3.2Gen1",
|
||||
"flashVendor": "USB",
|
||||
"guid": "0781-5583-8355-81071A2B0211",
|
||||
"guid": randomGuid,
|
||||
// "guid": "0781-5583-8355-81071A2B0211",
|
||||
"keyfile": "DUMMY_KEYFILE",
|
||||
"lanIp": "192.168.254.36",
|
||||
"license": "",
|
||||
@@ -54,7 +55,7 @@ const serverState = {
|
||||
"pluginInstalled": false,
|
||||
"registered": true,
|
||||
"regGen": 0,
|
||||
"regGuid": "0781-5583-8355-81071A2B0211",
|
||||
// "regGuid": "0781-5583-8355-81071A2B0211",
|
||||
"site": "http://localhost:4321",
|
||||
"state": state,
|
||||
"theme": {
|
||||
|
||||
@@ -98,7 +98,7 @@ const ariaLablledById = computed((): string|undefined => props.title ? `ModalTit
|
||||
</header>
|
||||
<slot name="main"></slot>
|
||||
|
||||
<footer class="text-14px relative -mx-16px -mb-16px sm:-mx-24px sm:-mb-24px p-4 sm:p-6">
|
||||
<footer v-if="$slots['footer']" class="text-14px relative -mx-16px -mb-16px sm:-mx-24px sm:-mb-24px 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>
|
||||
|
||||
@@ -5,15 +5,18 @@ import '~/assets/main.css';
|
||||
|
||||
import { useCallbackActionsStore } from '~/store/callbackActions';
|
||||
import { usePromoStore } from '~/store/promo';
|
||||
import { useTrialStore } from '~/store/trial';
|
||||
|
||||
const { callbackStatus } = storeToRefs(useCallbackActionsStore());
|
||||
const { promoVisible } = storeToRefs(usePromoStore());
|
||||
const { trialStatus } = storeToRefs(useTrialStore());
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative z-[99999]">
|
||||
<UpcCallbackFeedback :open="callbackStatus !== 'ready'" />
|
||||
<UpcPromo :open="promoVisible" />
|
||||
<UpcTrial :open="trialStatus === 'requestNew' || trialStatus === 'failed'" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -83,6 +83,9 @@ const subheading = computed(() => {
|
||||
if (callbackStatus.value === 'success') {
|
||||
if (accountActionType.value === 'signIn') return `You're one step closer to enhancing your Unraid experience`;
|
||||
if (keyActionType.value === 'purchase') return `Thank you for purchasing an Unraid ${keyType.value} Key!`;
|
||||
if (keyActionType.value === 'replace') return `Your ${keyType.value} Key has been replaced!`;
|
||||
if (keyActionType.value === 'trialExtend') return `Your Trial key has been extended!`;
|
||||
if (keyActionType.value === 'trialStart') return `Your free Trial key provides all the functionality of a Pro Registration key`;
|
||||
if (keyActionType.value === 'upgrade') return `Thank you for upgrading to an Unraid ${keyType.value} Key!`;
|
||||
return '';
|
||||
}
|
||||
@@ -148,7 +151,7 @@ const { text, copy, copied, isSupported } = useClipboard({ source: keyUrl.value
|
||||
<UpcCallbackFeedbackStatus
|
||||
v-if="showPromoCta"
|
||||
:icon="InformationCircleIcon"
|
||||
:text="'Enhance your Unraid experience with Unraid Connect'" />
|
||||
:text="'Enhance your experience with Unraid Connect'" />
|
||||
|
||||
<UpcCallbackFeedbackStatus
|
||||
v-if="showSignInCta"
|
||||
|
||||
@@ -4,15 +4,17 @@ import { useServerStore } from '~/store/server';
|
||||
import 'tailwindcss/tailwind.css';
|
||||
import '~/assets/main.css';
|
||||
|
||||
const { expireTime, pluginInstalled, state, stateData } = storeToRefs(useServerStore());
|
||||
const { expireTime, pluginInstalled, registered, state, stateData } = storeToRefs(useServerStore());
|
||||
|
||||
const showConnectCopy = computed(() => (pluginInstalled.value && !registered.value));
|
||||
|
||||
const heading = computed(() => {
|
||||
if (pluginInstalled.value) return 'Thank you for installing Connect!';
|
||||
if (showConnectCopy.value) return 'Thank you for installing Connect!';
|
||||
return stateData.value.heading;
|
||||
});
|
||||
|
||||
const subheading = computed(() => {
|
||||
if (pluginInstalled.value) return 'Sign In to your Unraid.net account to get started';
|
||||
if (showConnectCopy.value) return 'Sign In to your Unraid.net account to get started';
|
||||
return stateData.value.message;
|
||||
});
|
||||
|
||||
@@ -23,7 +25,7 @@ const showExpireTime = computed(() => {
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-y-24px w-full min-w-300px md:min-w-[500px] max-w-4xl p-16px">
|
||||
<header :class="{ 'text-center': pluginInstalled }">
|
||||
<header :class="{ 'text-center': showConnectCopy }">
|
||||
<h2 class="text-24px text-center font-semibold" v-html="heading" />
|
||||
<div v-html="subheading" class="flex flex-col gap-y-8px" />
|
||||
<UpcUptimeExpire v-if="showExpireTime" class="opacity-75 mt-12px" />
|
||||
|
||||
66
components/UserProfile/Trial.vue
Normal file
66
components/UserProfile/Trial.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<script lang="ts" setup>
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useTrialStore } from "~/store/trial";
|
||||
|
||||
export interface Props {
|
||||
open?: boolean;
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
open: false,
|
||||
});
|
||||
|
||||
const trialStore = useTrialStore();
|
||||
const { trialStatus } = storeToRefs(trialStore);
|
||||
|
||||
const heading = computed(() => {
|
||||
if (trialStatus.value === 'failed') return 'Failed to start your free 30 day trial';
|
||||
if (trialStatus.value === 'requestNew') return 'Starting your free 30 day trial…';
|
||||
if (trialStatus.value === 'success') return 'Free 30 Day Trial Created';
|
||||
return '';
|
||||
});
|
||||
const subheading = computed(() => {
|
||||
/** @todo show response error */
|
||||
if (trialStatus.value === 'failed') return 'Key server did not return a trial key. Please try again later.';
|
||||
if (trialStatus.value === 'requestNew') return 'Please wait while and keep this window open';
|
||||
if (trialStatus.value === 'success') return 'Please wait while the page reloads to install your trial key';
|
||||
return '';
|
||||
});
|
||||
|
||||
const close = () => {
|
||||
if (trialStatus.value === 'requestNew') return console.debug("[close] not allowed");
|
||||
trialStore.setTrialStatus('ready');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
@close="close"
|
||||
:open="open"
|
||||
:title="heading"
|
||||
:description="subheading"
|
||||
:show-close-x="trialStatus !== 'requestNew'"
|
||||
max-width="max-w-640px"
|
||||
>
|
||||
<template #main>
|
||||
<BrandLoading v-if="trialStatus === 'requestNew'" class="w-[150px] mx-auto my-24px" />
|
||||
<div v-if="trialStatus === 'failed'" class="my-24px">
|
||||
<p class="text-red"></p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="trialStatus !== 'requestNew'" #footer>
|
||||
<div class="w-full max-w-xs flex flex-col items-center gap-y-16px mx-auto">
|
||||
<div>
|
||||
<button
|
||||
@click="close"
|
||||
class="text-12px tracking-wide inline-block mx-8px opacity-60 hover:opacity-100 focus:opacity-100 underline transition"
|
||||
:title="'Close Modal'"
|
||||
>
|
||||
{{ "Close" }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
15
composables/preventClose.ts
Normal file
15
composables/preventClose.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export const preventClose = (e: { preventDefault: () => void; returnValue: string; }) => {
|
||||
e.preventDefault();
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
e.returnValue = '';
|
||||
// eslint-disable-next-line no-alert
|
||||
alert('Closing this pop-up window while actions are being preformed may lead to unintended errors.');
|
||||
};
|
||||
|
||||
export const addPreventClose = () => {
|
||||
window.addEventListener('beforeunload', preventClose);
|
||||
};
|
||||
|
||||
export const removePreventClose = () => {
|
||||
window.removeEventListener('beforeunload', preventClose);
|
||||
};
|
||||
33
composables/services/keyServer.ts
Normal file
33
composables/services/keyServer.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { request } from '~/composables/services/request';
|
||||
|
||||
const KeyServer = request.url('https://keys.lime-technology.com');
|
||||
|
||||
export interface StartTrialPayload {
|
||||
guid: string;
|
||||
timestamp: number; // timestamp in seconds
|
||||
}
|
||||
export interface StartTrialResponse {
|
||||
license?: string;
|
||||
trial?: string
|
||||
};
|
||||
export const startTrial = (payload: StartTrialPayload) => KeyServer
|
||||
.url('/account/trial')
|
||||
.formUrl(payload)
|
||||
.post();
|
||||
|
||||
export interface KeyServerTroubleshootPayload {
|
||||
email: string;
|
||||
subject: string;
|
||||
message: string;
|
||||
guid?: string; // if passed it'll be appended to the email subject instead of date/time
|
||||
comments?: string; // HONEYPOT FIELD. Passing a non-empty value for 'comments' will trigger the honeypot, thus not send an email but won't return any errors.
|
||||
}
|
||||
export const troubleshoot = (payload: KeyServerTroubleshootPayload) => KeyServer
|
||||
.url('/ips/troubleshoot')
|
||||
.formUrl(payload)
|
||||
.post();
|
||||
|
||||
export const validateGuid = (payload: { guid: string }) => KeyServer
|
||||
.url('/validate/guid')
|
||||
.formUrl(payload)
|
||||
.post();
|
||||
@@ -104,16 +104,16 @@ export const useCallbackStoreGeneric = (
|
||||
defineStore('callback', () => {
|
||||
const callbackActions = useCallbackActions();
|
||||
const encryptionKey = 'Uyv2o8e*FiQe8VeLekTqyX6Z*8XonB';
|
||||
const sendType = 'fromUpc';
|
||||
const defaultSendType = 'fromUpc';
|
||||
|
||||
const send = (url: string, payload: SendPayloads) => {
|
||||
const send = (url: string, payload: SendPayloads, sendType?: 'fromUpc' | 'forUpc') => {
|
||||
console.debug('[callback.send]');
|
||||
const stringifiedData = JSON.stringify({
|
||||
actions: [
|
||||
...payload,
|
||||
],
|
||||
sender: window.location.href,
|
||||
type: sendType,
|
||||
type: sendType ?? defaultSendType,
|
||||
});
|
||||
const encryptedMessage = AES.encrypt(stringifiedData, encryptionKey).toString();
|
||||
// build and go to url
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
import { useAccountStore } from './account';
|
||||
import { useInstallKeyStore } from './installKey';
|
||||
import { useCallbackStoreGeneric, type ExternalPayload, type ExternalKeyActions, type QueryPayloads } from './callback';
|
||||
import { addPreventClose, removePreventClose } from '~/composables/preventClose';
|
||||
import { useAccountStore } from '~/store/account';
|
||||
import { useInstallKeyStore } from '~/store/installKey';
|
||||
import { useCallbackStoreGeneric, type ExternalPayload, type ExternalKeyActions, type QueryPayloads } from '~/store/callback';
|
||||
import { remove } from '@vue/shared';
|
||||
// import { useServerStore } from './server';
|
||||
|
||||
export const useCallbackActionsStore = defineStore(
|
||||
@@ -59,21 +61,13 @@ export const useCallbackActionsStore = defineStore(
|
||||
|
||||
const setCallbackStatus = (status: CallbackStatus) => callbackStatus.value = status;
|
||||
|
||||
const preventClose = (e: { preventDefault: () => void; returnValue: string; }) => {
|
||||
e.preventDefault();
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
e.returnValue = '';
|
||||
// eslint-disable-next-line no-alert
|
||||
alert('Closing this pop-up window while actions are being preformed may lead to unintended errors.');
|
||||
};
|
||||
|
||||
watch(callbackStatus, (newVal, _oldVal) => {
|
||||
watch(callbackStatus, (newVal, oldVal) => {
|
||||
console.debug('[callbackStatus]', newVal);
|
||||
if (newVal === 'ready') {
|
||||
window.addEventListener('beforeunload', preventClose);
|
||||
if (newVal === 'loading') {
|
||||
addPreventClose();
|
||||
}
|
||||
if (newVal !== 'ready') {
|
||||
window.removeEventListener('beforeunload', preventClose);
|
||||
if (oldVal === 'loading') {
|
||||
removePreventClose();
|
||||
// removing query string once actions are done so users can't refresh the page and go through the same actions
|
||||
window.history.replaceState(null, '', window.location.pathname);
|
||||
}
|
||||
|
||||
@@ -185,6 +185,9 @@ export const useServerStore = defineStore('server', () => {
|
||||
name: 'signIn',
|
||||
text: 'Sign In with Unraid.net Account',
|
||||
};
|
||||
/**
|
||||
* @todo implment conditional sign out to show that a keyfile is required
|
||||
*/
|
||||
const signOutAction: ServerStateDataAction = {
|
||||
click: () => { accountStore.signOut() },
|
||||
external: true,
|
||||
@@ -200,7 +203,7 @@ export const useServerStore = defineStore('server', () => {
|
||||
text: 'Extend Trial',
|
||||
};
|
||||
const trialStartAction: ServerStateDataAction = {
|
||||
click: () => { trialStore.start() },
|
||||
click: () => { trialStore.setTrialStatus('requestNew') },
|
||||
external: true,
|
||||
icon: KeyIcon,
|
||||
name: 'trialStart',
|
||||
|
||||
@@ -1,17 +1,28 @@
|
||||
import { defineStore, createPinia, setActivePinia } from 'pinia';
|
||||
import { useCallbackStore } from './callbackActions';
|
||||
import { useServerStore } from './server';
|
||||
|
||||
import { addPreventClose, removePreventClose } from '~/composables/preventClose';
|
||||
import { startTrial, type StartTrialResponse } from '~/composables/services/keyServer';
|
||||
|
||||
import { useCallbackStore, useCallbackActionsStore } from '~/store/callbackActions';
|
||||
import { useDropdownStore } from '~/store/dropdown';
|
||||
import { useServerStore } from '~/store/server';
|
||||
import type { ExternalPayload } from '~/store/callback';
|
||||
|
||||
/**
|
||||
* @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 useTrialStore = defineStore('trial', () => {
|
||||
const callbackStore = useCallbackStore();
|
||||
const callbackActionsStore = useCallbackActionsStore();
|
||||
const dropdownStore = useDropdownStore();
|
||||
const serverStore = useServerStore();
|
||||
|
||||
type TrialStatus = 'failed' | 'ready' | 'requestNew' | 'success';
|
||||
const trialStatus = ref<TrialStatus>('ready');
|
||||
|
||||
const extend = () => {
|
||||
console.debug('[extend]');
|
||||
callbackStore.send('https://localhost:8008/connect', [{
|
||||
@@ -21,20 +32,65 @@ export const useTrialStore = defineStore('trial', () => {
|
||||
type: 'trialExtend',
|
||||
}]);
|
||||
};
|
||||
const start = () => {
|
||||
console.debug('[start]');
|
||||
callbackStore.send('https://localhost:8008/connect', [{
|
||||
server: {
|
||||
...serverStore.serverAccountPayload,
|
||||
},
|
||||
type: 'trialStart',
|
||||
}]);
|
||||
|
||||
// @todo post to key server
|
||||
const requestTrialNew = async () => {
|
||||
console.debug('[requestTrialNew]');
|
||||
try {
|
||||
const payload = {
|
||||
guid: serverStore.guid,
|
||||
timestamp: Math.floor(Date.now() / 1000),
|
||||
};
|
||||
const response: StartTrialResponse = await startTrial(payload).json();
|
||||
console.debug('[requestTrialNew]', response);
|
||||
if (!response.license) {
|
||||
trialStatus.value = 'failed';
|
||||
return console.error('[requestTrialNew]', 'No license returned', response);
|
||||
}
|
||||
// manually create a payload to mimic a callback for key installs
|
||||
const trialStartData: ExternalPayload = {
|
||||
actions: [
|
||||
{
|
||||
keyUrl: response.license,
|
||||
type: 'trialStart',
|
||||
},
|
||||
],
|
||||
sender: window.location.href,
|
||||
type: 'forUpc',
|
||||
};
|
||||
console.debug('[requestTrialNew]', trialStartData);
|
||||
trialStatus.value = 'success';
|
||||
return callbackActionsStore.redirectToCallbackType(trialStartData);
|
||||
} catch (error) {
|
||||
trialStatus.value = 'failed';
|
||||
console.error('[requestTrialNew]', error);
|
||||
}
|
||||
};
|
||||
|
||||
const setTrialStatus = (status: TrialStatus) => trialStatus.value = status;
|
||||
|
||||
watch(trialStatus, (newVal, oldVal) => {
|
||||
console.debug('[trialStatus]', newVal, oldVal);
|
||||
// opening
|
||||
if (newVal === 'requestNew') {
|
||||
addPreventClose();
|
||||
dropdownStore.dropdownHide(); // close the dropdown when the trial modal is opened
|
||||
setTimeout(() => {
|
||||
requestTrialNew();
|
||||
}, 1500);
|
||||
}
|
||||
// allow closure
|
||||
if (newVal === 'failed' || newVal === 'success') {
|
||||
removePreventClose();
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
// State
|
||||
trialStatus,
|
||||
// Actions
|
||||
extend,
|
||||
start,
|
||||
requestTrialNew,
|
||||
setTrialStatus,
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user