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 / views / _components / recaptcha / Recaptcha.vue

Recaptcha.vue in The Innovative Form Builder – IvyForms 1.1, at frontend/src/views/_components/recaptcha/Recaptcha.vue

252 lines 5.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <!-- eslint-disable vue/multi-word-component-names -->
2 <template>
3 <div
4 ref="recaptchaContainer"
5 class="ivyforms-recaptcha-container ivyforms-flex ivyforms-justify-content-start ivyforms-align-items-center"
6 :class="{
7 'ivyforms-recaptcha-container--invisible':
8 config?.type === 'invisible' || config?.type === 'v3',
9 'ivyforms-mb-18': !props.error,
10 }"
11 ></div>
12 </template>
13
14 <script setup lang="ts">
15 import { ref, onMounted, onUnmounted, watch } from 'vue'
16 import { recaptchaService } from '@/services/recaptcha'
17 import { useLabels } from '@/composables/useLabels'
18 import type { RecaptchaConfig } from '@/types/recaptcha/recaptcha-interface'
19
20 interface Props {
21 config: RecaptchaConfig
22 modelValue?: string
23 error?: string
24 }
25
26 interface Emits {
27 (event: 'update:modelValue', value: string): void
28 (event: 'verify', token: string): void
29 (event: 'expire'): void
30 (event: 'error', error: Error): void
31 }
32
33 const props = defineProps<Props>()
34 const emit = defineEmits<Emits>()
35
36 const recaptchaContainer = ref<HTMLDivElement>()
37 const widgetId = ref<number | null>(null)
38 const isLoading = ref(true)
39 const internalError = ref<string | null>(null)
40
41 const { getLabel } = useLabels()
42
43 // Initialize reCAPTCHA when component mounts
44 onMounted(async () => {
45 try {
46 await initRecaptcha()
47 } catch (err) {
48 internalError.value = err instanceof Error ? err.message : getLabel('recaptcha_init_failed')
49 emit('error', err instanceof Error ? err : new Error(getLabel('recaptcha_init_failed')))
50 }
51 })
52
53 // Clean up when component unmounts
54 onUnmounted(() => {
55 if (widgetId.value !== null) {
56 recaptchaService.reset()
57 }
58 })
59
60 // Watch for config changes and reinitialize
61 watch(
62 () => props.config,
63 async (newConfig) => {
64 if (newConfig) {
65 await initRecaptcha()
66 }
67 },
68 { deep: true },
69 )
70
71 /**
72 * Initialize reCAPTCHA widget
73 */
74 async function initRecaptcha() {
75 if (!props.config || !recaptchaContainer.value) {
76 return
77 }
78
79 try {
80 isLoading.value = true
81 internalError.value = null
82
83 // Initialize the service with config
84 await recaptchaService.init(props.config)
85
86 // Render the widget based on type
87 if (props.config.type === 'v3' || props.config.type === 'invisible') {
88 // For v3 and invisible, we don't render a visible widget
89 // For invisible, we still need to render it but with size: 'invisible'
90 if (props.config.type === 'invisible') {
91 const options = {
92 callback: onVerify,
93 'expired-callback': onExpire,
94 'error-callback': onError,
95 size: 'invisible' as const,
96 }
97 widgetId.value = await recaptchaService.render(recaptchaContainer.value, options)
98 }
99 isLoading.value = false
100 } else {
101 // For v2 checkbox, render the visible widget
102 const options = {
103 callback: onVerify,
104 'expired-callback': onExpire,
105 'error-callback': onError,
106 }
107
108 widgetId.value = await recaptchaService.render(recaptchaContainer.value, options)
109 isLoading.value = false
110 }
111 } catch (err) {
112 internalError.value =
113 err instanceof Error ? err.message : getLabel('recaptcha_initialization_failed')
114 isLoading.value = false
115 emit(
116 'error',
117 err instanceof Error ? err : new Error(getLabel('recaptcha_initialization_failed')),
118 )
119 }
120 }
121
122 /**
123 * Handle successful verification
124 */
125 function onVerify(token: string) {
126 emit('update:modelValue', token)
127 emit('verify', token)
128 }
129
130 /**
131 * Handle token expiration
132 */
133 function onExpire() {
134 emit('update:modelValue', '')
135 emit('expire')
136 }
137
138 /**
139 * Handle reCAPTCHA errors
140 */
141 function onError() {
142 const error = new Error(getLabel('recaptcha_verification_failed'))
143 emit('update:modelValue', '')
144 emit('error', error)
145 }
146
147 /**
148 * Execute reCAPTCHA (for v3 and invisible)
149 */
150 async function execute(action: string = 'submit'): Promise<string> {
151 if (props.config.type === 'v3') {
152 try {
153 const token = await recaptchaService.execute(action)
154 emit('update:modelValue', token)
155 emit('verify', token)
156 return token
157 } catch (err) {
158 emit('error', err instanceof Error ? err : new Error(getLabel('recaptcha_v3_failed')))
159 throw err
160 }
161 } else if (props.config.type === 'invisible') {
162 try {
163 const token = await recaptchaService.execute()
164 emit('update:modelValue', token)
165 emit('verify', token)
166 return token
167 } catch (err) {
168 emit('error', err instanceof Error ? err : new Error(getLabel('recaptcha_invisible_failed')))
169 throw err
170 }
171 } else {
172 // For v2, get the current response
173 const token = recaptchaService.getResponse()
174 if (!token) {
175 throw new Error(getLabel('recaptcha_complete_verification'))
176 }
177 return token
178 }
179 }
180
181 /**
182 * Reset reCAPTCHA widget
183 */
184 function reset() {
185 recaptchaService.reset()
186 emit('update:modelValue', '')
187 }
188
189 /**
190 * Get current response token
191 */
192 function getResponse(): string {
193 return recaptchaService.getResponse()
194 }
195
196 // Expose methods for parent components
197 defineExpose({
198 execute,
199 reset,
200 getResponse,
201 isLoading: () => isLoading.value,
202 hasError: () => !!internalError.value,
203 getError: () => internalError.value,
204 })
205 </script>
206
207 <style lang="scss" scoped>
208 .ivyforms-recaptcha-container {
209 min-height: 78px; // Standard reCAPTCHA height
210
211 // Hide for v3 (invisible) and invisible v2
212 &:empty {
213 min-height: 0;
214 display: none;
215 }
216
217 // Hide invisible reCAPTCHA containers
218 &--invisible {
219 min-height: 0;
220 display: none;
221 }
222
223 :deep(.g-recaptcha) {
224 transform-origin: 0 0;
225 }
226
227 // Dark theme support
228 &.dark-theme {
229 :deep(.g-recaptcha) {
230 filter: invert(1) hue-rotate(180deg);
231 }
232 }
233
234 @keyframes pulse {
235 0%,
236 100% {
237 opacity: 1;
238 }
239 50% {
240 opacity: 0.5;
241 }
242 }
243 }
244
245 // Error state
246 .ivyforms-recaptcha-error {
247 color: var(--map-status-error-fill-0);
248 font-size: 14px;
249 margin-top: 4px;
250 }
251 </style>
252