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

127 lines 4.0 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 || 'starter')
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 if (w.ivyFormsProScripts === undefined) return
58
59 _state.loading.value = true
60 // Preserve previous successful state unless force refresh
61 if (options.force) {
62 _state.error.value = null
63 }
64
65 const { data, error, status } = await request<ProFeaturePayload>('pro/features')
66
67 if (data) {
68 _state.data.value = data as ProFeaturePayload
69 _state.loaded.value = true
70 _state.fetchedAt.value = Date.now()
71 _state.error.value = null
72 } else if (error) {
73 // Graceful fallback scenarios when Pro is not installed / inaccessible.
74 // We always flip loaded=true so UI logic can settle and show locked tiles instead of staying pending forever.
75 if (status === 404) {
76 // Route not found => Pro not installed. Treat as Lite + empty features.
77 // The 404 error above is expected and can be safely ignored.
78 console.info(
79 '%c🌿 IvyForms Lite %cis in use',
80 'font-weight: bold; color: #06a192;',
81 'font-weight: normal; color: inherit;',
82 )
83 _state.data.value = _state.data.value || {
84 plan: 'lite',
85 features: {},
86 upgrade: {},
87 }
88 _state.error.value = null
89 } else if (status === 401 || status === 403) {
90 // Unauthorized: maybe nonce expired or user lacks capability. Still expose empty dataset.
91 _state.data.value = _state.data.value || {
92 plan: 'unauthorized',
93 features: {},
94 upgrade: {},
95 }
96 _state.error.value = { message: 'pro_features_unauthorized', status }
97 } else {
98 // Other server / network errors. Provide safe empty dataset if none yet so gating can proceed.
99 if (!_state.data.value) {
100 _state.data.value = {
101 plan: 'error',
102 features: {},
103 upgrade: {},
104 }
105 }
106 _state.error.value = error
107 }
108 _state.loaded.value = true
109 if (!_state.fetchedAt.value) _state.fetchedAt.value = Date.now()
110 }
111 _state.loading.value = false
112 }
113
114 const refresh = async () => fetch({ force: true })
115
116 return {
117 ..._state,
118 plan,
119 license,
120 groups,
121 upgradeMap,
122 hasFeature,
123 fetch,
124 refresh,
125 }
126 }
127