PluginProbe
The Innovative Form Builder – IvyForms / 0.8.2
The Innovative Form Builder – IvyForms v0.8.2
1.5.1 1.5 1.4.1 1.4 trunk 0.1.2 0.2 0.2.1 0.3 0.3.1 0.4 0.5 0.6 0.6.1 0.6.1-backup 0.6.1.1 0.7 0.8 0.8.1 0.8.2 0.9 0.9.1 1.0 1.1 1.1.1 All 27 releases
ivyforms / frontend / src / composables / useProFeatures.ts

useProFeatures.ts in The Innovative Form Builder – IvyForms 0.8.2, at frontend/src/composables/useProFeatures.ts

140 lines 4.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { ref, computed } from 'vue'
2 import { useApiClient } from '@/composables/useApiClient'
3
4 /**
5 * Pro Features Composable
6 * Responsibilities:
7 * - Provides reactive access to plan, feature flags, license info, upgrade map & groups
8 * - Offers refresh() to revalidate after license activation/upgrade without page reload
9 *
10 * Usage:
11 * const pro = useProFeatures();
12 * await pro.fetch(); // load once per page view
13 * if (pro.hasFeature('password_field')) { // enable feature-specific UI }
14 *
15 * Staleness Strategy:
16 * - Current implementation treats first successful load as final until refresh() is called.
17 * - You can add TTL logic: if(Date.now() - fetchedAt > X) then refetch lazily.
18 */
19 interface ProFeaturePayload {
20 plan: string
21 features: Record<string, boolean>
22 upgrade: Record<string, string>
23 groups?: Record<string, string[]>
24 meta?: Record<string, { label?: string; group?: string }>
25 license?: Record<string, unknown>
26 }
27
28 interface Window {
29 ivyFormsProScripts?: Record<string, string>
30 }
31
32 const _state = {
33 loading: ref(false),
34 loaded: ref(false),
35 error: ref<unknown | null>(null),
36 data: ref<ProFeaturePayload | null>(null),
37 fetchedAt: ref<number | null>(null),
38 }
39
40 export function useProFeatures() {
41 const { request } = useApiClient()
42
43 const hasFeature = (slug: string) => {
44 if (!_state.loaded.value) return false
45 return !!_state.data.value?.features?.[slug]
46 }
47
48 const plan = computed(() => _state.data.value?.plan || 'lite')
49 const license = computed(() => _state.data.value?.license || null)
50 const upgradeMap = computed(() => _state.data.value?.upgrade || {})
51 const groups = computed(() => _state.data.value?.groups || {})
52
53 const fetch = async (options: { force?: boolean } = {}) => {
54 if (_state.loading.value) return
55 if (!options.force && _state.loaded.value) return
56 const w = window as Window
57
58 // If Pro scripts are not loaded, mark as loaded with lite plan to avoid infinite pending
59 if (w.ivyFormsProScripts === undefined) {
60 _state.data.value = {
61 plan: 'lite',
62 features: {},
63 upgrade: {},
64 }
65 _state.loaded.value = true
66 _state.fetchedAt.value = Date.now()
67 return
68 }
69
70 _state.loading.value = true
71 // Preserve previous successful state unless force refresh
72 if (options.force) {
73 _state.error.value = null
74 }
75
76 const { data, error, status } = await request<{ message: string; data: ProFeaturePayload }>(
77 'pro/features',
78 )
79
80 if (data && data.data) {
81 _state.data.value = data.data as ProFeaturePayload
82 _state.loaded.value = true
83 _state.fetchedAt.value = Date.now()
84 _state.error.value = null
85 } else if (error) {
86 // Graceful fallback scenarios when Pro is not installed / inaccessible.
87 // We always flip loaded=true so UI logic can settle and show locked tiles instead of staying pending forever.
88 if (status === 404) {
89 // Route not found => Pro not installed. Treat as Lite + empty features.
90 // The 404 error above is expected and can be safely ignored.
91 console.info(
92 '%c🌿 IvyForms Lite %cis in use',
93 'font-weight: bold; color: #06a192;',
94 'font-weight: normal; color: inherit;',
95 )
96 _state.data.value = _state.data.value || {
97 plan: 'lite',
98 features: {},
99 upgrade: {},
100 }
101 _state.error.value = null
102 } else if (status === 401 || status === 403) {
103 // Unauthorized: maybe nonce expired or user lacks capability. Still expose empty dataset.
104 _state.data.value = _state.data.value || {
105 plan: 'unauthorized',
106 features: {},
107 upgrade: {},
108 }
109 _state.error.value = { message: 'pro_features_unauthorized', status }
110 } else {
111 // Other server / network errors. Provide safe empty dataset if none yet so gating can proceed.
112 if (!_state.data.value) {
113 _state.data.value = {
114 plan: 'error',
115 features: {},
116 upgrade: {},
117 }
118 }
119 _state.error.value = error
120 }
121 _state.loaded.value = true
122 if (!_state.fetchedAt.value) _state.fetchedAt.value = Date.now()
123 }
124 _state.loading.value = false
125 }
126
127 const refresh = async () => fetch({ force: true })
128
129 return {
130 ..._state,
131 plan,
132 license,
133 groups,
134 upgradeMap,
135 hasFeature,
136 fetch,
137 refresh,
138 }
139 }
140