PluginProbe
The Innovative Form Builder – IvyForms / 0.8.1
The Innovative Form Builder – IvyForms v0.8.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 / services / turnstile.ts

turnstile.ts in The Innovative Form Builder – IvyForms 0.8.1, at frontend/src/services/turnstile.ts

212 lines 5.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Turnstile Service for Frontend Integration
3 * Handles Cloudflare Turnstile integration
4 */
5
6 import type { TurnstileWidgetOptions } from '@/types/turnstile/turnstile-interface'
7 import { useLabels } from '@/composables/useLabels'
8
9 /**
10 * Helper function to get labels safely (defers useLabels call until runtime)
11 */
12 function getLabel(key: string): string {
13 try {
14 const { getLabel: getLabelFn } = useLabels()
15 return getLabelFn(key)
16 } catch {
17 return key // Fallback if Pinia not ready
18 }
19 }
20
21 export interface TurnstileInstance {
22 render: (container: string | HTMLElement, options: TurnstileWidgetOptions) => string
23 execute: (container?: string | HTMLElement) => void
24 reset: (container?: string | HTMLElement) => void
25 remove: (container?: string | HTMLElement) => void
26 getResponse: (container?: string | HTMLElement) => string | undefined
27 isExpired: (container?: string | HTMLElement) => boolean
28 }
29
30 declare global {
31 interface Window {
32 turnstile?: TurnstileInstance
33 onTurnstileLoad?: () => void
34 }
35 }
36
37 export class TurnstileService {
38 private static instance: TurnstileService
39 private siteKey: string | null = null
40 private widgetId: string | null = null
41 private scriptLoaded = false
42 private language: string = 'auto'
43
44 static getInstance(): TurnstileService {
45 if (!TurnstileService.instance) {
46 TurnstileService.instance = new TurnstileService()
47 }
48 return TurnstileService.instance
49 }
50
51 /**
52 * Initialize Turnstile with configuration
53 * Note: Assumes Cloudflare Turnstile script is already loaded by ShortcodeService
54 */
55 async init(siteKey: string, language?: string): Promise<void> {
56 this.siteKey = siteKey
57 this.language = language || 'auto'
58
59 return new Promise((resolve) => {
60 if (window.turnstile) {
61 this.scriptLoaded = true
62 resolve()
63 } else {
64 // If script isn't loaded yet, wait a bit and try again
65 setTimeout(() => {
66 if (window.turnstile) {
67 this.scriptLoaded = true
68 resolve()
69 } else {
70 console.warn(getLabel('turnstile_script_not_found'))
71 resolve() // Resolve anyway to avoid hanging
72 }
73 }, 100)
74 }
75 })
76 }
77
78 /**
79 * Render Turnstile widget
80 */
81 async render(
82 container: string | HTMLElement,
83 options: Partial<TurnstileWidgetOptions> = {},
84 ): Promise<string> {
85 if (!window.turnstile || !this.siteKey) {
86 throw new Error(getLabel('turnstile_not_initialized'))
87 }
88
89 const renderOptions: TurnstileWidgetOptions = {
90 sitekey: this.siteKey,
91 callback: options.callback,
92 'expired-callback': options['expired-callback'],
93 'error-callback': options['error-callback'],
94 'timeout-callback': options['timeout-callback'],
95 size: options.size || 'normal',
96 theme: options.theme || 'auto',
97 language: this.language,
98 tabindex: options.tabindex,
99 'response-field': options['response-field'],
100 'response-field-name': options['response-field-name'],
101 }
102
103 return new Promise((resolve) => {
104 this.widgetId = window.turnstile!.render(container, renderOptions)
105 resolve(this.widgetId)
106 })
107 }
108
109 /**
110 * Execute Turnstile
111 */
112 async execute(): Promise<string> {
113 if (!window.turnstile) {
114 throw new Error(getLabel('turnstile_not_initialized'))
115 }
116
117 if (!this.widgetId) {
118 throw new Error(getLabel('turnstile_widget_not_initialized'))
119 }
120
121 return new Promise((resolve, reject) => {
122 try {
123 window.turnstile!.execute(this.widgetId!)
124
125 // Set timeout to reject if no response in 30 seconds
126 const timeout = setTimeout(() => {
127 reject(new Error(getLabel('turnstile_execution_timeout')))
128 }, 30000)
129
130 // Poll for response
131 const checkResponse = () => {
132 const response = this.getResponse()
133 if (response) {
134 clearTimeout(timeout)
135 resolve(response)
136 } else {
137 setTimeout(checkResponse, 100)
138 }
139 }
140 checkResponse()
141 } catch (error) {
142 reject(error)
143 }
144 })
145 }
146
147 /**
148 * Get response from Turnstile widget
149 */
150 getResponse(): string {
151 if (!window.turnstile || !this.widgetId) {
152 return ''
153 }
154
155 return window.turnstile.getResponse(this.widgetId) || ''
156 }
157
158 /**
159 * Reset Turnstile widget
160 */
161 reset(): void {
162 if (window.turnstile && this.widgetId) {
163 window.turnstile.reset(this.widgetId)
164 }
165 }
166
167 /**
168 * Remove Turnstile widget
169 */
170 remove(): void {
171 if (window.turnstile && this.widgetId) {
172 window.turnstile.remove(this.widgetId)
173 this.widgetId = null
174 }
175 }
176
177 /**
178 * Check if token is expired
179 */
180 isExpired(): boolean {
181 if (!window.turnstile || !this.widgetId) {
182 return true
183 }
184
185 return window.turnstile.isExpired(this.widgetId)
186 }
187
188 /**
189 * Get current widget ID
190 */
191 getWidgetId(): string | null {
192 return this.widgetId
193 }
194
195 /**
196 * Check if Turnstile is ready
197 */
198 isReady(): boolean {
199 return this.scriptLoaded && !!window.turnstile
200 }
201
202 /**
203 * Get current site key
204 */
205 getSiteKey(): string | null {
206 return this.siteKey
207 }
208 }
209
210 // Export singleton instance
211 export const turnstileService = TurnstileService.getInstance()
212