feat: start trial from upc

This commit is contained in:
Zack Spear
2023-07-06 17:46:09 -07:00
parent 77ede914a2
commit de9b76a6a6
12 changed files with 217 additions and 41 deletions

View File

@@ -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": {

View File

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

View File

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

View File

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

View File

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

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

View 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);
};

View 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();

View File

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

View File

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

View File

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

View File

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