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

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

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