main
vue 278 lines 7.83 KB
Raw
1 <template>
2 <n-spin :show="loading" class="min-h-48">
3 <n-empty v-if="errorMessage" :description="errorMessage" class="h-48 justify-center">
4 <template #icon>
5 <Icon :name="WarningIcon" />
6 </template>
7 </n-empty>
8 <template v-else>
9 <template v-if="!selectedSubscription">
10 <div v-if="availableSubscriptions.length" class="list flex flex-col gap-2">
11 <SubscriptionCard
12 v-for="subscription of availableSubscriptions"
13 :key="subscription.id"
14 :subscription
15 selectable
16 embedded
17 class="item-appear item-appear-bottom item-appear-005 cursor-pointer"
18 @click="selectedSubscription = subscription"
19 />
20 </div>
21 <template v-else>
22 <n-empty
23 v-if="!loading"
24 description="Congratulations, you have already unlocked all available features"
25 class="h-48 justify-center"
26 >
27 <template #icon>
28 <Icon :name="CheckIcon" />
29 </template>
30 </n-empty>
31 </template>
32 </template>
33 <template v-else>
34 <SubscriptionCard :subscription="selectedSubscription" embedded hide-details />
35 <div class="checkout-form item-appear item-appear-bottom item-appear-005 mt-8">
36 <n-spin :show="loadingLicense || loadingSession">
37 <n-form :label-width="80" :model="checkoutForm" :rules>
38 <div class="flex flex-col gap-1">
39 <n-form-item label="Company Name" path="company_name">
40 <n-input
41 v-model:value.trim="checkoutForm.company_name"
42 placeholder="Input Company Name..."
43 clearable
44 />
45 </n-form-item>
46 <n-form-item label="Email" path="customer_email">
47 <n-input
48 v-model:value.trim="checkoutForm.customer_email"
49 placeholder="Input email..."
50 clearable
51 />
52 </n-form-item>
53 <div class="flex justify-end gap-4">
54 <n-button quaternary @click="selectedSubscription = null">
55 <template #icon>
56 <Icon :name="ArrowLeftIcon" />
57 </template>
58 Back
59 </n-button>
60 <n-button
61 type="success"
62 :disabled="!isValid"
63 :loading="loadingSession"
64 @click="createCheckoutSession()"
65 >
66 <template #icon>
67 <Icon :name="CartIcon" />
68 </template>
69 Checkout
70 </n-button>
71 </div>
72 </div>
73 </n-form>
74 </n-spin>
75 </div>
76 </template>
77 </template>
78 </n-spin>
79 </template>
80
81 <script setup lang="ts">
82 import type { FormItemRule, FormRules } from "naive-ui"
83 import type { CheckoutPayload, License, LicenseCustomer, LicenseFeatures, SubscriptionFeature } from "@/types/license.d"
84 import { NButton, NEmpty, NForm, NFormItem, NInput, NSpin, useMessage } from "naive-ui"
85 import isEmail from "validator/es/lib/isEmail"
86 import { computed, onBeforeMount, ref, toRefs, watch } from "vue"
87 import Api from "@/api"
88 import Icon from "@/components/common/Icon.vue"
89 import SubscriptionCard from "./SubscriptionCard.vue"
90
91 const props = defineProps<{
92 featuresData?: LicenseFeatures[]
93 subscriptionsData?: SubscriptionFeature[]
94 }>()
95 const { featuresData, subscriptionsData } = toRefs(props)
96
97 const WarningIcon = "carbon:warning-alt"
98 const CartIcon = "carbon:shopping-cart"
99 const CheckIcon = "carbon:checkmark-outline"
100 const ArrowLeftIcon = "carbon:arrow-left"
101
102 const message = useMessage()
103 const loadingFeatures = ref(false)
104 const loadingSubscriptions = ref(false)
105 const loadingLicense = ref(false)
106 const loadingSession = ref(false)
107 const loading = computed(() => loadingFeatures.value || loadingSubscriptions.value)
108 const selectedSubscription = ref<SubscriptionFeature | null>(null)
109 const errorMessage = ref<string | null>(null)
110 const checkoutForm = ref<CheckoutPayload>(getCheckoutForm())
111
112 const license = ref<License | null>(null)
113 const featuresLoaded = ref<LicenseFeatures[]>([])
114 const subscriptionsLoaded = ref<SubscriptionFeature[]>([])
115 const features = computed(() => featuresLoaded.value || featuresData?.value || [])
116 const subscriptions = computed(() => subscriptionsLoaded.value || subscriptionsData?.value || [])
117 const availableSubscriptions = computed<SubscriptionFeature[]>(() =>
118 subscriptions.value.filter(o => !features.value.includes(o.name))
119 )
120 const isValid = computed(() => {
121 if (!checkoutForm.value.company_name) {
122 return false
123 }
124
125 if (!isEmail(checkoutForm.value.customer_email)) {
126 return false
127 }
128
129 return true
130 })
131
132 const rules: FormRules = {
133 company_name: {
134 required: true,
135 message: "Please input company name",
136 trigger: ["input", "blur"]
137 },
138 customer_email: {
139 required: true,
140 trigger: ["input", "blur"],
141 validator: (_rule: FormItemRule, value: string) => {
142 if (!value) {
143 return new Error("Email is required")
144 }
145 if (!isEmail(value)) {
146 return new Error("The email is not formatted correctly")
147 }
148 }
149 }
150 }
151
152 watch(selectedSubscription, val => {
153 if (val && !license.value) {
154 getLicense()
155 }
156 })
157
158 function getCheckoutForm(args?: {
159 email?: string
160 companyName?: string
161 customer?: LicenseCustomer
162 subscription?: SubscriptionFeature | null
163 }): CheckoutPayload {
164 const customerEmail = args?.email || args?.customer?.email || ""
165 const companyName = args?.companyName || args?.customer?.companyName || ""
166
167 return {
168 feature_id: args?.subscription?.id || 0,
169 cancel_url: `${location.origin}/license/cancel`,
170 success_url: `${location.origin}/license/success?email=${customerEmail}`,
171 customer_email: customerEmail,
172 company_name: companyName
173 }
174 }
175
176 function getLicenseFeatures() {
177 loadingFeatures.value = true
178
179 Api.license
180 .getLicenseFeatures()
181 .then(res => {
182 if (res.data.success) {
183 featuresLoaded.value = res.data?.features
184 } else {
185 message.warning(res.data?.message || "An error occurred. Please try again later.")
186 }
187 })
188 .catch(err => {
189 if (err.response.status !== 404) {
190 errorMessage.value = "We're sorry, there was an issue loading your license"
191 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
192 }
193 })
194 .finally(() => {
195 loadingFeatures.value = false
196 })
197 }
198
199 function getSubscriptionFeatures() {
200 loadingSubscriptions.value = true
201
202 Api.license
203 .getSubscriptionFeatures()
204 .then(res => {
205 if (res.data.success) {
206 subscriptionsLoaded.value = res.data?.features || []
207 } else {
208 message.warning(res.data?.message || "An error occurred. Please try again later.")
209 }
210 })
211 .catch(err => {
212 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
213 })
214 .finally(() => {
215 loadingSubscriptions.value = false
216 })
217 }
218
219 function getLicense() {
220 loadingLicense.value = true
221
222 Api.license
223 .verifyLicense()
224 .then(res => {
225 if (res.data.success) {
226 license.value = res.data?.license
227 checkoutForm.value = getCheckoutForm({ customer: license.value.customer })
228 } else {
229 message.warning(res.data?.message || "An error occurred. Please try again later.")
230 }
231 })
232 .catch(err => {
233 if (err.response.status !== 404) {
234 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
235 }
236 })
237 .finally(() => {
238 loadingLicense.value = false
239 })
240 }
241
242 function createCheckoutSession() {
243 loadingSession.value = true
244
245 const payload = getCheckoutForm({
246 email: checkoutForm.value.customer_email,
247 companyName: checkoutForm.value.company_name,
248 subscription: selectedSubscription.value
249 })
250
251 Api.license
252 .createCheckoutSession(payload)
253 .then(res => {
254 if (res.data.success) {
255 window.location.href = res.data.session.url
256 } else {
257 message.warning(res.data?.message || "An error occurred. Please try again later.")
258 }
259 })
260 .catch(err => {
261 if (err.response.status !== 404) {
262 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
263 }
264 })
265 .finally(() => {
266 loadingSession.value = false
267 })
268 }
269
270 onBeforeMount(() => {
271 if (!features.value.length) {
272 getLicenseFeatures()
273 }
274 if (!subscriptions.value.length) {
275 getSubscriptionFeatures()
276 }
277 })
278 </script>