main
vue 282 lines 7.37 KB
Raw
1 <template>
2 <div class="license-features" :class="{ loading }">
3 <div class="license-features-box flex items-center justify-center">
4 <n-spin :show="loading" class="h-full w-full" content-class="h-full">
5 <div v-if="!loading" class="wrapper flex h-full flex-col gap-4">
6 <div class="flex items-center justify-between gap-4">
7 <h3>
8 {{ features.length ? "Your features" : "Unlock features" }}
9 </h3>
10 <n-popover v-if="features.length" class="max-w-80" trigger="hover">
11 <template #trigger>
12 <Icon :name="InfoIcon" :size="20" class="cursor-help" />
13 </template>
14 To unsubscribe, click on the feature you wish to remove, and then click on the "Unsubscribe"
15 button.
16 </n-popover>
17 </div>
18 <div class="grow overflow-hidden">
19 <n-scrollbar v-if="!loading">
20 <div v-if="license" class="features-list flex flex-col gap-2">
21 <template v-if="activeSubscriptions.length">
22 <SubscriptionCard
23 v-for="subscription of activeSubscriptions"
24 :key="subscription.id"
25 :subscription
26 :license-data
27 embedded
28 show-delete-on-dialog
29 @deleted="load()"
30 />
31 </template>
32 <n-empty v-else description="No features unlocked" class="h-48 justify-center">
33 <template #icon>
34 <Icon :name="NoFeaturesIcon" />
35 </template>
36 </n-empty>
37 </div>
38 <LicenseCheckoutWizard v-else :features-data="features" />
39 </n-scrollbar>
40 </div>
41 <div v-if="license" class="cta-section">
42 <n-button type="primary" class="w-full!" size="large" @click="openCheckout()">
43 <template #icon>
44 <Icon :name="ExtendIcon" />
45 </template>
46 Add feature
47 </n-button>
48 </div>
49 </div>
50 </n-spin>
51 </div>
52
53 <div v-if="!hideKey && !loading" class="footer mt-3">
54 <template v-if="license">
55 <span class="cursor-pointer" @click="showLicenseDetails = true">
56 Your license:
57 <strong>{{ license }}</strong>
58 <Icon :name="InfoIcon" :size="14" class="relative top-0.5 ml-1" />
59 </span>
60 </template>
61 <template v-else>
62 <span class="cursor-pointer" @click="showLicenseUpload = true">
63 If you possess a license, you may load it by clicking here
64 </span>
65 </template>
66 </div>
67
68 <n-modal
69 v-model:show="showCheckoutForm"
70 preset="card"
71 :style="{ maxWidth: 'min(600px, 90vw)', minHeight: 'min(300px, 90vh)', overflow: 'hidden' }"
72 title="Add feature"
73 :bordered="false"
74 content-class="flex flex-col"
75 segmented
76 >
77 <LicenseCheckoutWizard :features-data="features" :subscriptions-data="subscriptions" />
78 </n-modal>
79
80 <n-modal
81 v-model:show="showLicenseDetails"
82 preset="card"
83 :style="{ maxWidth: 'min(600px, 90vw)', minHeight: 'min(300px, 90vh)', overflow: 'hidden' }"
84 title="License details"
85 :bordered="false"
86 content-class="flex flex-col"
87 segmented
88 >
89 <LicenseDetails v-if="license" :features-data="features" hide-features />
90 </n-modal>
91
92 <n-modal
93 v-model:show="showLicenseUpload"
94 preset="card"
95 :style="{ maxWidth: 'min(600px, 90vw)', minHeight: 'min(300px, 90vh)', overflow: 'hidden' }"
96 title="Upload your license"
97 :bordered="false"
98 content-class="flex flex-col"
99 segmented
100 >
101 <LicenseLoadForm @uploaded="licenseUploaded()" />
102 </n-modal>
103 </div>
104 </template>
105
106 <script setup lang="ts">
107 import type { License, LicenseFeatures, LicenseKey, SubscriptionFeature } from "@/types/license.d"
108 import { NButton, NEmpty, NModal, NPopover, NScrollbar, NSpin, useMessage } from "naive-ui"
109 import { computed, onBeforeMount, onMounted, ref, toRefs } from "vue"
110 import Api from "@/api"
111 import Icon from "@/components/common/Icon.vue"
112 import LicenseCheckoutWizard from "./LicenseCheckoutWizard.vue"
113 import LicenseDetails from "./LicenseDetails.vue"
114 import LicenseLoadForm from "./LicenseLoadForm.vue"
115 import SubscriptionCard from "./SubscriptionCard.vue"
116
117 const props = defineProps<{
118 hideKey?: boolean
119 licenseData?: License
120 }>()
121
122 const emit = defineEmits<{
123 (e: "licenseKeyLoaded", value: LicenseKey): void
124 (
125 e: "mounted",
126 value: {
127 reload: () => void
128 }
129 ): void
130 }>()
131
132 const { hideKey, licenseData } = toRefs(props)
133
134 const InfoIcon = "carbon:information"
135 const ExtendIcon = "carbon:intent-request-create"
136 const NoFeaturesIcon = "carbon:intent-request-uninstall"
137
138 const message = useMessage()
139 const showCheckoutForm = ref(false)
140 const showLicenseDetails = ref(false)
141 const showLicenseUpload = ref(false)
142 const loadingLicense = ref(false)
143 const loadingFeatures = ref(false)
144 const loadingSubscriptions = ref(false)
145
146 const license = ref<LicenseKey | null>(null)
147 const features = ref<LicenseFeatures[]>([])
148 const subscriptions = ref<SubscriptionFeature[]>([])
149
150 const loading = computed(() => loadingLicense.value || loadingFeatures.value || loadingSubscriptions.value)
151
152 const activeSubscriptions = computed<SubscriptionFeature[]>(() =>
153 features.value.map(f => subscriptions.value.find(s => s.name === f) as SubscriptionFeature).filter(o => !!o)
154 )
155
156 function getLicense() {
157 loadingLicense.value = true
158
159 Api.license
160 .getLicense()
161 .then(res => {
162 if (res.data.success) {
163 license.value = res.data?.license_key
164 if (license.value) {
165 emit("licenseKeyLoaded", license.value)
166 }
167 } else {
168 message.warning(res.data?.message || "An error occurred. Please try again later.")
169 }
170 })
171 .catch(err => {
172 if (err.response.status !== 404) {
173 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
174 }
175 })
176 .finally(() => {
177 loadingLicense.value = false
178 })
179 }
180
181 function getLicenseFeatures() {
182 loadingFeatures.value = true
183
184 Api.license
185 .getLicenseFeatures()
186 .then(res => {
187 if (res.data.success) {
188 features.value = res.data?.features
189 if (features.value.length) {
190 getSubscriptionFeatures()
191 }
192 } else {
193 message.warning(res.data?.message || "An error occurred. Please try again later.")
194 }
195 })
196 .catch(err => {
197 if (err.response.status !== 404) {
198 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
199 }
200 })
201 .finally(() => {
202 loadingFeatures.value = false
203 })
204 }
205
206 function getSubscriptionFeatures() {
207 loadingSubscriptions.value = true
208
209 Api.license
210 .getSubscriptionFeatures()
211 .then(res => {
212 if (res.data.success) {
213 subscriptions.value = res.data?.features || []
214 } else {
215 message.warning(res.data?.message || "An error occurred. Please try again later.")
216 }
217 })
218 .catch(err => {
219 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
220 })
221 .finally(() => {
222 loadingSubscriptions.value = false
223 })
224 }
225
226 function openCheckout() {
227 showCheckoutForm.value = true
228 }
229
230 function licenseUploaded() {
231 showLicenseUpload.value = false
232 load()
233 }
234
235 function load() {
236 getLicense()
237 getLicenseFeatures()
238 }
239
240 onBeforeMount(() => {
241 load()
242 })
243
244 onMounted(() => {
245 emit("mounted", {
246 reload: load
247 })
248 })
249 </script>
250
251 <style lang="scss" scoped>
252 .license-features {
253 display: flex;
254 flex-direction: column;
255 overflow: hidden;
256
257 &.loading {
258 min-height: 300px;
259 min-width: 300px;
260 }
261
262 .license-features-box {
263 background-color: var(--bg-default-color);
264 border-radius: var(--border-radius);
265 border: 1px solid var(--border-color);
266 padding: 18px;
267 flex-grow: 1;
268 overflow: hidden;
269 }
270 .footer {
271 width: 100%;
272 text-align: center;
273 font-size: 12px;
274
275 .cursor-pointer {
276 &:hover {
277 color: var(--primary-color);
278 }
279 }
280 }
281 }
282 </style>