| 1 |
<template> |
| 2 |
<div class="ivyforms-recaptcha-field ivyforms-mx-0 ivyforms-my-20"> |
| 3 |
<!-- Show reCAPTCHA widget if configured --> |
| 4 |
<Recaptcha |
| 5 |
v-if="isRecaptchaConfigured" |
| 6 |
ref="recaptchaWidgetRef" |
| 7 |
v-model="recaptchaToken" |
| 8 |
:config="recaptchaConfig" |
| 9 |
@verify="onRecaptchaVerify" |
| 10 |
@expire="onRecaptchaExpire" |
| 11 |
@error="onRecaptchaError" |
| 12 |
/> |
| 13 |
|
| 14 |
<!-- Show error message if present --> |
| 15 |
<div v-if="recaptchaError"> |
| 16 |
<IvyAlert :description="recaptchaError" type="error" /> |
| 17 |
</div> |
| 18 |
|
| 19 |
<!-- Show warning message if not configured --> |
| 20 |
<div v-else-if="!isRecaptchaConfigured" class="ivyforms-recaptcha-not-configured"> |
| 21 |
<IvyAlert :description="getLabel('recaptcha_not_configured')" type="warning" /> |
| 22 |
</div> |
| 23 |
</div> |
| 24 |
</template> |
| 25 |
|
| 26 |
<script setup lang="ts"> |
| 27 |
import { computed, onMounted, ref } from 'vue' |
| 28 |
import { useLabels } from '@/composables/useLabels' |
| 29 |
import Recaptcha from '@/views/_components/recaptcha/Recaptcha.vue' |
| 30 |
import IvyAlert from '@/views/_components/alert/IvyAlert.vue' |
| 31 |
import type { Field } from '@/types/field' |
| 32 |
import type { RecaptchaConfig, WindowRecaptchaConfig } from '@/types/recaptcha/recaptcha-interface' |
| 33 |
import type { RecaptchaType } from '@/types/recaptcha/recaptcha-type' |
| 34 |
|
| 35 |
declare global { |
| 36 |
interface Window { |
| 37 |
wpIvyRecaptchaConfig?: WindowRecaptchaConfig | unknown[] |
| 38 |
} |
| 39 |
} |
| 40 |
|
| 41 |
interface Props { |
| 42 |
field: Field |
| 43 |
modelValue?: string |
| 44 |
error?: string |
| 45 |
} |
| 46 |
|
| 47 |
interface Emits { |
| 48 |
(event: 'update:modelValue', value: string): void |
| 49 |
} |
| 50 |
|
| 51 |
const props = defineProps<Props>() |
| 52 |
const emit = defineEmits<Emits>() |
| 53 |
|
| 54 |
const { getLabel } = useLabels() |
| 55 |
const recaptchaToken = ref('') |
| 56 |
const recaptchaError = ref('') |
| 57 |
|
| 58 |
// Check if reCAPTCHA is configured |
| 59 |
const isRecaptchaConfigured = computed(() => { |
| 60 |
// Check if global reCAPTCHA config exists and is configured |
| 61 |
if (typeof window !== 'undefined' && window.wpIvyRecaptchaConfig) { |
| 62 |
const config = window.wpIvyRecaptchaConfig as WindowRecaptchaConfig |
| 63 |
|
| 64 |
// Handle both empty array case and proper object case |
| 65 |
if (!config || Array.isArray(config) || typeof config !== 'object') { |
| 66 |
return false |
| 67 |
} |
| 68 |
|
| 69 |
// Check if CAPTCHA is enabled |
| 70 |
const isEnabled = |
| 71 |
config.enabled === true || |
| 72 |
config.enabled === 'true' || |
| 73 |
config.enabled === '1' || |
| 74 |
config.enabled === 1 |
| 75 |
|
| 76 |
// Check if provider is recaptcha |
| 77 |
const isRecaptchaProvider = config.provider === 'recaptcha' |
| 78 |
|
| 79 |
// Check if recaptcha configuration exists and is configured |
| 80 |
const hasRecaptchaConfig = config.recaptcha && typeof config.recaptcha === 'object' |
| 81 |
|
| 82 |
if (!hasRecaptchaConfig) { |
| 83 |
return false |
| 84 |
} |
| 85 |
|
| 86 |
const recaptchaConfig = config.recaptcha |
| 87 |
|
| 88 |
// Check if properly configured - handle both boolean and string values |
| 89 |
const isConfigured = |
| 90 |
recaptchaConfig.configured === true || |
| 91 |
recaptchaConfig.configured === 'true' || |
| 92 |
recaptchaConfig.configured === '1' || |
| 93 |
recaptchaConfig.configured === 1 |
| 94 |
|
| 95 |
// Check if it has valid site key (the actual fix!) |
| 96 |
const hasSiteKey = |
| 97 |
recaptchaConfig.siteKey && |
| 98 |
typeof recaptchaConfig.siteKey === 'string' && |
| 99 |
recaptchaConfig.siteKey.trim() !== '' |
| 100 |
|
| 101 |
return isEnabled && isRecaptchaProvider && isConfigured && hasSiteKey |
| 102 |
} |
| 103 |
|
| 104 |
return false |
| 105 |
}) |
| 106 |
|
| 107 |
// reCAPTCHA configuration from global settings |
| 108 |
const recaptchaConfig = computed((): RecaptchaConfig => { |
| 109 |
if (typeof window !== 'undefined' && window.wpIvyRecaptchaConfig) { |
| 110 |
const config = window.wpIvyRecaptchaConfig as WindowRecaptchaConfig |
| 111 |
// Only process if config is a proper object (not array) and has recaptcha config |
| 112 |
if (config && typeof config === 'object' && !Array.isArray(config) && config.recaptcha) { |
| 113 |
const recaptchaData = config.recaptcha |
| 114 |
return { |
| 115 |
siteKey: recaptchaData.siteKey || '', |
| 116 |
type: (recaptchaData.type || 'v2') as RecaptchaType, |
| 117 |
theme: (recaptchaData.theme || 'light') as 'light' | 'dark', |
| 118 |
language: recaptchaData.language || 'en', |
| 119 |
} |
| 120 |
} |
| 121 |
} |
| 122 |
|
| 123 |
return { |
| 124 |
siteKey: '', |
| 125 |
type: 'v2' as RecaptchaType, |
| 126 |
theme: 'light' as const, |
| 127 |
language: 'en', |
| 128 |
} |
| 129 |
}) |
| 130 |
|
| 131 |
// Handle reCAPTCHA verification |
| 132 |
const onRecaptchaVerify = (token: string) => { |
| 133 |
recaptchaToken.value = token |
| 134 |
recaptchaError.value = '' // Clear any previous errors |
| 135 |
emit('update:modelValue', token) |
| 136 |
} |
| 137 |
|
| 138 |
// Handle reCAPTCHA expiration |
| 139 |
const onRecaptchaExpire = () => { |
| 140 |
recaptchaToken.value = '' |
| 141 |
recaptchaError.value = getLabel('recaptcha_expired') |
| 142 |
emit('update:modelValue', '') |
| 143 |
} |
| 144 |
|
| 145 |
// Handle reCAPTCHA errors |
| 146 |
const onRecaptchaError = (error: Error) => { |
| 147 |
console.error('reCAPTCHA error:', error) |
| 148 |
recaptchaToken.value = '' |
| 149 |
recaptchaError.value = error.message |
| 150 |
emit('update:modelValue', '') |
| 151 |
} |
| 152 |
|
| 153 |
// For v3, we need to execute the reCAPTCHA when the form is submitted |
| 154 |
const recaptchaWidgetRef = ref<InstanceType<typeof Recaptcha>>() |
| 155 |
|
| 156 |
// Expose method for parent components to trigger v3 execution |
| 157 |
const executeRecaptcha = async (action = 'submit') => { |
| 158 |
recaptchaError.value = '' |
| 159 |
|
| 160 |
if (!recaptchaWidgetRef.value) { |
| 161 |
const errorMsg = getLabel('recaptcha_widget_not_initialized_field') |
| 162 |
recaptchaError.value = errorMsg |
| 163 |
throw new Error(errorMsg) |
| 164 |
} |
| 165 |
|
| 166 |
if (recaptchaConfig.value.type === 'v3') { |
| 167 |
try { |
| 168 |
return await recaptchaWidgetRef.value.execute(action) |
| 169 |
} catch (error) { |
| 170 |
const errorMsg = getLabel('recaptcha_v3_failed') |
| 171 |
console.error(errorMsg, error) |
| 172 |
recaptchaError.value = errorMsg |
| 173 |
throw error |
| 174 |
} |
| 175 |
} else if (recaptchaConfig.value.type === 'invisible') { |
| 176 |
try { |
| 177 |
return await recaptchaWidgetRef.value.execute() |
| 178 |
} catch (error) { |
| 179 |
const errorMsg = getLabel('recaptcha_invisible_failed') |
| 180 |
console.error(errorMsg, error) |
| 181 |
recaptchaError.value = errorMsg |
| 182 |
throw error |
| 183 |
} |
| 184 |
} |
| 185 |
|
| 186 |
// For v2 checkbox, check if we have a token |
| 187 |
const currentToken = recaptchaToken.value |
| 188 |
if (!currentToken) { |
| 189 |
const errorMsg = getLabel('recaptcha_complete_verification') |
| 190 |
recaptchaError.value = errorMsg |
| 191 |
throw new Error(errorMsg) |
| 192 |
} |
| 193 |
|
| 194 |
return currentToken |
| 195 |
} |
| 196 |
|
| 197 |
// Expose the execute method for parent components |
| 198 |
defineExpose({ |
| 199 |
executeRecaptcha, |
| 200 |
}) |
| 201 |
|
| 202 |
// Initialize with current value |
| 203 |
onMounted(() => { |
| 204 |
if (props.modelValue) { |
| 205 |
recaptchaToken.value = props.modelValue |
| 206 |
recaptchaError.value = '' // Clear any errors when we have a valid value |
| 207 |
} |
| 208 |
}) |
| 209 |
</script> |
| 210 |
|