PluginProbe
The Innovative Form Builder – IvyForms / 1.1
The Innovative Form Builder – IvyForms v1.1
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 1.1, at frontend/src/composables/useProFeatures.ts

155 lines 4.9 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
52 /**
53 * The plan code stored in the database regardless of license active status.
54 * Use this to determine if a user has purchased a plan tier (even if their
55 * license is currently inactive / not yet activated).
56 * Returns 'lite' when Pro is not installed or no plan data is available.
57 */
58 const installedPlan = computed<string>(() => {
59 const lic = _state.data.value?.license
60 if (lic && typeof (lic as Record<string, unknown>).plan === 'string') {
61 return (lic as Record<string, string>).plan
62 }
63 return 'lite'
64 })
65 const groups = computed(() => _state.data.value?.groups || {})
66
67 const fetch = async (options: { force?: boolean } = {}) => {
68 if (_state.loading.value) return
69 if (!options.force && _state.loaded.value) return
70 const w = window as Window
71
72 // If Pro scripts are not loaded, mark as loaded with lite plan to avoid infinite pending
73 if (w.ivyFormsProScripts === undefined) {
74 _state.data.value = {
75 plan: 'lite',
76 features: {},
77 upgrade: {},
78 }
79 _state.loaded.value = true
80 _state.fetchedAt.value = Date.now()
81 return
82 }
83
84 _state.loading.value = true
85 // Preserve previous successful state unless force refresh
86 if (options.force) {
87 _state.error.value = null
88 }
89
90 const { data, error, status } = await request<{ message: string; data: ProFeaturePayload }>(
91 'pro/features',
92 )
93
94 if (data && data.data) {
95 _state.data.value = data.data as ProFeaturePayload
96 _state.loaded.value = true
97 _state.fetchedAt.value = Date.now()
98 _state.error.value = null
99 } else if (error) {
100 // Graceful fallback scenarios when Pro is not installed / inaccessible.
101 // We always flip loaded=true so UI logic can settle and show locked tiles instead of staying pending forever.
102 if (status === 404) {
103 // Route not found => Pro not installed. Treat as Lite + empty features.
104 // The 404 error above is expected and can be safely ignored.
105 console.info(
106 '%c🌿 IvyForms Lite %cis in use',
107 'font-weight: bold; color: #06a192;',
108 'font-weight: normal; color: inherit;',
109 )
110 _state.data.value = _state.data.value || {
111 plan: 'lite',
112 features: {},
113 upgrade: {},
114 }
115 _state.error.value = null
116 } else if (status === 401 || status === 403) {
117 // Unauthorized: maybe nonce expired or user lacks capability. Still expose empty dataset.
118 _state.data.value = _state.data.value || {
119 plan: 'unauthorized',
120 features: {},
121 upgrade: {},
122 }
123 _state.error.value = { message: 'pro_features_unauthorized', status }
124 } else {
125 // Other server / network errors. Provide safe empty dataset if none yet so gating can proceed.
126 if (!_state.data.value) {
127 _state.data.value = {
128 plan: 'error',
129 features: {},
130 upgrade: {},
131 }
132 }
133 _state.error.value = error
134 }
135 _state.loaded.value = true
136 if (!_state.fetchedAt.value) _state.fetchedAt.value = Date.now()
137 }
138 _state.loading.value = false
139 }
140
141 const refresh = async () => fetch({ force: true })
142
143 return {
144 ..._state,
145 plan,
146 license,
147 installedPlan,
148 groups,
149 upgradeMap,
150 hasFeature,
151 fetch,
152 refresh,
153 }
154 }
155