@redacto.io/consent-sdk-react 3.5.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -171,8 +171,11 @@ export type ConsentEventPayload = {
171
171
  source?: string;
172
172
  declined?: boolean;
173
173
  meta_data?: {
174
- specific_uuid: string;
174
+ specific_uuid?: string;
175
+ minor_age?: number;
175
176
  };
177
+ guardian_verification_reference?: string;
178
+ self_declared_adult?: boolean;
176
179
  };
177
180
 
178
181
  export type DataElement = {
@@ -199,22 +202,11 @@ export type SubmitConsentEventParams = {
199
202
  purposes: Array<Purpose>;
200
203
  declined: boolean;
201
204
  meta_data?: {
202
- specific_uuid: string;
205
+ specific_uuid?: string;
206
+ minor_age?: number;
203
207
  };
204
- };
205
-
206
- export type SubmitGuardianInfoParams = {
207
- accessToken: string;
208
- baseUrl?: string;
209
- guardianName: string;
210
- guardianContact: string;
211
- guardianRelationship: string;
212
- };
213
-
214
- export type GuardianInfoPayload = {
215
- guardian_name: string;
216
- guardian_contact: string;
217
- guardian_relationship: string;
208
+ guardianVerificationReference?: string;
209
+ selfDeclaredAdult?: boolean;
218
210
  };
219
211
 
220
212
  // UI Component Types
@@ -330,3 +322,133 @@ export type FetchTTSAudioUrlsParams = {
330
322
  noticeUuid: string;
331
323
  language: string;
332
324
  };
325
+
326
+ // =============================================================================
327
+ // Guardian Verification Types (aligned with backend API)
328
+ // =============================================================================
329
+
330
+ /**
331
+ * Parameters for initiating guardian verification
332
+ * POST /guardian/initiate-verification
333
+ */
334
+ export type InitiateGuardianVerificationParams = {
335
+ accessToken: string;
336
+ baseUrl?: string;
337
+ guardianName: string;
338
+ guardianContact: string;
339
+ guardianRelationship: string;
340
+ frontendCallbackUrl?: string;
341
+ metadata?: Record<string, unknown>;
342
+ signal?: AbortSignal;
343
+ };
344
+
345
+ /**
346
+ * Response from initiate verification API
347
+ *
348
+ * Two shapes:
349
+ * 1. New verification needed: already_verified=false, includes session_token + digilocker_redirect_url
350
+ * 2. Already verified: already_verified=true, includes verification_reference — skip DigiLocker
351
+ */
352
+ export type InitiateGuardianVerificationResponse = {
353
+ success: boolean;
354
+ guardian_info_uuid: string;
355
+ already_verified: boolean;
356
+ /** Present only when already_verified=true */
357
+ verification_reference?: string;
358
+ /** Present only when already_verified=false (new flow) */
359
+ session_token?: string;
360
+ /** Present only when already_verified=false (new flow) */
361
+ digilocker_redirect_url?: string;
362
+ /** Present only when already_verified=false (new flow) */
363
+ expires_at?: string;
364
+ };
365
+
366
+ /**
367
+ * Parameters for checking verification status
368
+ * POST /guardian/verify-status
369
+ */
370
+ export type VerifyGuardianStatusParams = {
371
+ accessToken: string;
372
+ baseUrl?: string;
373
+ sessionToken: string;
374
+ signal?: AbortSignal;
375
+ };
376
+
377
+ /**
378
+ * Guardian details returned after successful verification
379
+ */
380
+ export type GuardianVerificationDetails = {
381
+ verified_name: string | null;
382
+ is_guardian_adult: boolean | null;
383
+ verified_at: string;
384
+ verification_reference: string | null;
385
+ };
386
+
387
+ /**
388
+ * Response from verify status API
389
+ */
390
+ export type VerifyGuardianStatusResponse = {
391
+ status: "pending" | "in_progress" | "verified" | "failed" | "expired";
392
+ guardian_info_uuid: string | null;
393
+ verification_reference: string | null;
394
+ guardian_details: GuardianVerificationDetails | null;
395
+ error: string | null;
396
+ error_code: GuardianVerificationErrorCode | null;
397
+ can_retry: boolean;
398
+ };
399
+
400
+ /**
401
+ * Error codes from guardian verification
402
+ */
403
+ export type GuardianVerificationErrorCode =
404
+ | "SESSION_EXPIRED"
405
+ | "GUARDIAN_UNDER_18"
406
+ | "TOKEN_FAILED"
407
+ | "DIGILOCKER_AUTH_FAILED"
408
+ | "NO_AUTH_CODE";
409
+
410
+ // =============================================================================
411
+ // Session Storage Types for redirect flow
412
+ // =============================================================================
413
+
414
+ /**
415
+ * State persisted during DigiLocker redirect flow.
416
+ * NOTE: accessToken is intentionally excluded — never store tokens in sessionStorage.
417
+ * The host app must provide the token when the component re-mounts.
418
+ */
419
+ export type PersistedConsentState = {
420
+ flowState: ConsentFlowState;
421
+ sessionToken?: string;
422
+ guardianInfoUuid?: string;
423
+ verificationReference?: string;
424
+ selectedPurposes?: Record<string, boolean>;
425
+ selectedDataElements?: Record<string, boolean>;
426
+ timestamp: string;
427
+ originalUrl: string;
428
+ verificationResult?: "pending" | "verified" | "failed" | "in_progress" | "expired";
429
+ noticeId?: string;
430
+ baseUrl?: string;
431
+ };
432
+
433
+ export type ConsentFlowState =
434
+ | "age-check"
435
+ | "guardian-details"
436
+ | "digilocker-pending"
437
+ | "consent-form"
438
+ | "submitting"
439
+ | "success"
440
+ | "error";
441
+
442
+ // =============================================================================
443
+ // DigiLocker Callback Types
444
+ // =============================================================================
445
+
446
+ /**
447
+ * Props for DigiLockerCallback component
448
+ */
449
+ export type DigiLockerCallbackProps = {
450
+ accessToken: string;
451
+ baseUrl?: string;
452
+ onSuccess?: (verificationReference: string) => void;
453
+ onError?: (error: Error, errorCode?: GuardianVerificationErrorCode) => void;
454
+ };
@@ -0,0 +1,536 @@
1
+ /**
2
+ * DigiLockerCallback Component
3
+ *
4
+ * Standalone component for handling DigiLocker redirect callbacks.
5
+ * Clients should add this to their routing (e.g., /digilocker-callback page).
6
+ *
7
+ * Usage in Next.js:
8
+ * ```tsx
9
+ * // pages/digilocker-callback.tsx
10
+ * import { DigiLockerCallback } from '@redacto.io/consent-sdk-react';
11
+ *
12
+ * export default function DigiLockerCallbackPage() {
13
+ * return <DigiLockerCallback accessToken={"<token>"} />;
14
+ * }
15
+ * ```
16
+ *
17
+ * Usage in React Router:
18
+ * ```tsx
19
+ * <Route path="/digilocker-callback" element={<DigiLockerCallback accessToken={"<token>"} />} />
20
+ * ```
21
+ *
22
+ * URL Parameters expected from redirect:
23
+ * - session_token: The verification session token
24
+ * - status: "verified" | "failed" | "expired"
25
+ * - verification_reference: (if verified) Reference for consent submission
26
+ * - error_code: (if failed) Error code like GUARDIAN_UNDER_18, SESSION_EXPIRED, etc.
27
+ */
28
+
29
+ import React, { useCallback, useEffect, useRef, useState } from "react";
30
+ import { verifyGuardianStatus } from "../api";
31
+ import type { DigiLockerCallbackProps, GuardianVerificationErrorCode } from "../api/types";
32
+ import { injectCheckboxStyles } from "../injectStyles";
33
+ import {
34
+ clearConsentState,
35
+ getConsentState,
36
+ updateConsentState,
37
+ } from "../services/storage";
38
+
39
+ type CallbackStatus = "processing" | "success" | "error";
40
+
41
+ export const DigiLockerCallback: React.FC<DigiLockerCallbackProps> = ({
42
+ accessToken: propAccessToken,
43
+ baseUrl,
44
+ onSuccess,
45
+ onError,
46
+ }) => {
47
+ const [status, setStatus] = useState<CallbackStatus>("processing");
48
+ const [message, setMessage] = useState("Verifying your guardian...");
49
+ const [errorDetail, setErrorDetail] = useState<string | null>(null);
50
+ const retryTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
51
+ const redirectTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
52
+ const isMountedRef = useRef(true);
53
+
54
+ const getErrorMessage = (errorCode?: GuardianVerificationErrorCode): string => {
55
+ switch (errorCode) {
56
+ case "GUARDIAN_UNDER_18":
57
+ return "Guardian verification failed: The guardian must be 18 years or older.";
58
+ case "SESSION_EXPIRED":
59
+ return "Verification session has expired. Please restart the verification process.";
60
+ case "TOKEN_FAILED":
61
+ return "DigiLocker authentication failed. Please try again.";
62
+ case "DIGILOCKER_AUTH_FAILED":
63
+ return "DigiLocker authorization was cancelled or failed. Please try again.";
64
+ case "NO_AUTH_CODE":
65
+ return "Missing authorization code from DigiLocker. Please try again.";
66
+ default:
67
+ return "Verification failed. Please try again.";
68
+ }
69
+ };
70
+
71
+ const redirectToOriginalPage = useCallback(
72
+ (
73
+ originalUrl: string,
74
+ verificationResult: "verified" | "failed" | "expired",
75
+ verificationReference?: string
76
+ ) => {
77
+ // Update persisted state with verification result
78
+ updateConsentState({
79
+ verificationResult,
80
+ verificationReference,
81
+ flowState: verificationResult === "verified" ? "consent-form" : "error",
82
+ });
83
+
84
+ // Redirect back to original page
85
+ redirectTimeoutRef.current = setTimeout(() => {
86
+ window.location.href = originalUrl;
87
+ }, 1500);
88
+ },
89
+ []
90
+ );
91
+
92
+ const handleCallback = useCallback(async () => {
93
+ try {
94
+ // 1. Extract URL parameters (new format from backend)
95
+ const params = new URLSearchParams(window.location.search);
96
+ const sessionToken = params.get("session_token");
97
+ const urlStatus = params.get("status") as "verified" | "failed" | "expired" | null;
98
+ const verificationReference = params.get("verification_reference");
99
+ const errorCode = params.get("error_code") as GuardianVerificationErrorCode | null;
100
+
101
+ // 2. Retrieve persisted state (optional — popup mode may not have saved state)
102
+ const persistedState = getConsentState();
103
+
104
+ const originalUrl = persistedState?.originalUrl || "/";
105
+ const storedBaseUrl = persistedState?.baseUrl;
106
+ const storedSessionToken = persistedState?.sessionToken;
107
+
108
+ // Use accessToken from props (never stored in sessionStorage for security)
109
+ const accessToken = propAccessToken;
110
+ if (!accessToken) {
111
+ throw new Error("Access token not provided. Please pass accessToken as a prop.");
112
+ }
113
+
114
+ // Use session token from URL (source of truth) or stored state as fallback
115
+ const activeSessionToken = sessionToken || storedSessionToken;
116
+ if (!activeSessionToken) {
117
+ throw new Error("Session token not found. Please restart the verification process.");
118
+ }
119
+
120
+ // Check if we're in an iframe (for iframe mode)
121
+ // Wrapped in try-catch: cross-origin iframes throw SecurityError on window.top access
122
+ let isInIframe = false;
123
+ try {
124
+ isInIframe = window.self !== window.top;
125
+ } catch {
126
+ isInIframe = true;
127
+ }
128
+
129
+ // 3. If status is already in URL params (direct redirect from backend)
130
+ if (urlStatus === "verified" && verificationReference) {
131
+ setStatus("success");
132
+ setMessage("Guardian verified successfully! Redirecting...");
133
+ onSuccess?.(verificationReference);
134
+
135
+ // If in iframe, postMessage to parent instead of redirecting
136
+ if (isInIframe && window.parent) {
137
+ window.parent.postMessage(
138
+ {
139
+ type: "DIGILOCKER_VERIFICATION_STATUS",
140
+ status: "verified",
141
+ verificationReference,
142
+ },
143
+ window.location.origin
144
+ );
145
+ return;
146
+ }
147
+
148
+ redirectToOriginalPage(originalUrl, "verified", verificationReference);
149
+ return;
150
+ }
151
+
152
+ if (urlStatus === "failed" || urlStatus === "expired") {
153
+ const urlErrorCode = errorCode ?? undefined;
154
+ const errorMessage = getErrorMessage(urlErrorCode);
155
+ setStatus("error");
156
+ setMessage("Verification failed");
157
+ setErrorDetail(errorMessage);
158
+ onError?.(new Error(errorMessage), urlErrorCode);
159
+
160
+ // If in iframe, postMessage to parent instead of redirecting
161
+ if (isInIframe && window.parent) {
162
+ window.parent.postMessage(
163
+ {
164
+ type: "DIGILOCKER_VERIFICATION_STATUS",
165
+ status: urlStatus,
166
+ errorCode: urlErrorCode,
167
+ error: errorMessage,
168
+ },
169
+ window.location.origin
170
+ );
171
+ return;
172
+ }
173
+
174
+ redirectToOriginalPage(originalUrl, urlStatus);
175
+ return;
176
+ }
177
+
178
+ // 4. If no status in URL, poll the backend for verification status
179
+ setMessage("Processing verification...");
180
+
181
+ const apiBaseUrl = baseUrl || storedBaseUrl;
182
+
183
+ const response = await verifyGuardianStatus({
184
+ accessToken,
185
+ baseUrl: apiBaseUrl,
186
+ sessionToken: activeSessionToken,
187
+ });
188
+ if (!isMountedRef.current) return;
189
+
190
+ if (response.status === "verified") {
191
+ // Use top-level verification_reference first, fall back to guardian_details
192
+ const verificationRef = response.verification_reference || response.guardian_details?.verification_reference || undefined;
193
+
194
+ if (!verificationRef) {
195
+ setStatus("error");
196
+ setMessage("Verification failed");
197
+ setErrorDetail("Verification completed but no reference received. Please try again.");
198
+ onError?.(new Error("Verification completed but no reference received."));
199
+ redirectToOriginalPage(originalUrl, "failed");
200
+ return;
201
+ }
202
+
203
+ setStatus("success");
204
+ setMessage("Guardian verified successfully! Redirecting...");
205
+ onSuccess?.(verificationRef);
206
+
207
+ // If in iframe, postMessage to parent instead of redirecting
208
+ if (isInIframe && window.parent) {
209
+ window.parent.postMessage(
210
+ {
211
+ type: "DIGILOCKER_VERIFICATION_STATUS",
212
+ status: "verified",
213
+ verificationReference: verificationRef,
214
+ },
215
+ window.location.origin
216
+ );
217
+ return;
218
+ }
219
+
220
+ redirectToOriginalPage(originalUrl, "verified", verificationRef);
221
+ } else if (response.status === "failed" || response.status === "expired") {
222
+ const responseErrorCode = response.error_code ?? undefined;
223
+ const errorMessage = response.error || getErrorMessage(responseErrorCode);
224
+ setStatus("error");
225
+ setMessage("Verification failed");
226
+ setErrorDetail(errorMessage);
227
+ onError?.(new Error(errorMessage), responseErrorCode);
228
+
229
+ // If in iframe, postMessage to parent instead of redirecting
230
+ if (isInIframe && window.parent) {
231
+ window.parent.postMessage(
232
+ {
233
+ type: "DIGILOCKER_VERIFICATION_STATUS",
234
+ status: response.status,
235
+ errorCode: responseErrorCode,
236
+ error: errorMessage,
237
+ },
238
+ window.location.origin
239
+ );
240
+ return;
241
+ }
242
+
243
+ redirectToOriginalPage(originalUrl, response.status);
244
+ } else {
245
+ // Still in progress - this shouldn't happen on callback page
246
+ // but handle it gracefully
247
+ setMessage("Verification is still in progress. Please wait...");
248
+ // Poll again after a short delay
249
+ // Retry with exponential backoff before giving up
250
+ const MAX_RETRIES = 3;
251
+ let retryCount = 0;
252
+
253
+ const attemptRetry = () => {
254
+ retryCount++;
255
+ const delay = 3000 * Math.pow(2, retryCount - 1); // 3s, 6s, 12s
256
+ retryTimeoutRef.current = setTimeout(async () => {
257
+ if (!isMountedRef.current) return;
258
+ try {
259
+ const retryResponse = await verifyGuardianStatus({
260
+ accessToken,
261
+ baseUrl: apiBaseUrl,
262
+ sessionToken: activeSessionToken,
263
+ });
264
+ if (!isMountedRef.current) return;
265
+
266
+ const retryRef = retryResponse.verification_reference || retryResponse.guardian_details?.verification_reference || undefined;
267
+
268
+ if (retryResponse.status === "verified" && retryRef) {
269
+ setStatus("success");
270
+ setMessage("Guardian verified successfully! Redirecting...");
271
+ onSuccess?.(retryRef);
272
+
273
+ if (isInIframe && window.parent) {
274
+ window.parent.postMessage(
275
+ {
276
+ type: "DIGILOCKER_VERIFICATION_STATUS",
277
+ status: "verified",
278
+ verificationReference: retryRef,
279
+ },
280
+ window.location.origin
281
+ );
282
+ return;
283
+ }
284
+
285
+ redirectToOriginalPage(originalUrl, "verified", retryRef);
286
+ } else if (retryResponse.status === "failed" || retryResponse.status === "expired") {
287
+ const retryErrorCode = retryResponse.error_code ?? undefined;
288
+ const retryErrorMessage = retryResponse.error || getErrorMessage(retryErrorCode) || "Verification could not be completed. Please try again.";
289
+ setStatus("error");
290
+ setMessage("Verification failed");
291
+ setErrorDetail(retryErrorMessage);
292
+ onError?.(new Error(retryErrorMessage), retryErrorCode);
293
+
294
+ if (isInIframe && window.parent) {
295
+ window.parent.postMessage(
296
+ {
297
+ type: "DIGILOCKER_VERIFICATION_STATUS",
298
+ status: retryResponse.status,
299
+ errorCode: retryErrorCode,
300
+ error: retryErrorMessage,
301
+ },
302
+ window.location.origin
303
+ );
304
+ return;
305
+ }
306
+
307
+ redirectToOriginalPage(originalUrl, retryResponse.status);
308
+ } else if (retryCount < MAX_RETRIES) {
309
+ setMessage("Verification taking longer than expected. Please wait...");
310
+ attemptRetry();
311
+ } else {
312
+ const timeoutMessage = "Verification could not be completed in time. Please try again.";
313
+ setStatus("error");
314
+ setMessage("Verification failed");
315
+ setErrorDetail(timeoutMessage);
316
+ onError?.(new Error(timeoutMessage));
317
+ redirectToOriginalPage(originalUrl, "failed");
318
+ }
319
+ } catch (retryErr) {
320
+ if (!isMountedRef.current) return;
321
+ if (retryCount < MAX_RETRIES) {
322
+ attemptRetry();
323
+ } else {
324
+ setStatus("error");
325
+ setMessage("Verification failed");
326
+ setErrorDetail((retryErr as Error).message);
327
+ onError?.(retryErr as Error);
328
+ redirectToOriginalPage(originalUrl, "failed");
329
+ }
330
+ }
331
+ }, delay);
332
+ };
333
+
334
+ attemptRetry();
335
+ }
336
+ } catch (error) {
337
+ console.error("DigiLocker callback error:", error);
338
+ setStatus("error");
339
+
340
+ const err = error as Error;
341
+ setMessage("Verification failed");
342
+ setErrorDetail(err.message || "An error occurred during verification");
343
+
344
+ onError?.(err);
345
+
346
+ // Try to redirect back even on error
347
+ const persistedState = getConsentState();
348
+ if (persistedState?.originalUrl) {
349
+ redirectTimeoutRef.current = setTimeout(() => {
350
+ redirectToOriginalPage(persistedState.originalUrl, "failed");
351
+ }, 3000);
352
+ }
353
+ }
354
+ }, [propAccessToken, baseUrl, onSuccess, onError, redirectToOriginalPage]);
355
+
356
+ useEffect(() => {
357
+ injectCheckboxStyles();
358
+ handleCallback();
359
+ return () => {
360
+ isMountedRef.current = false;
361
+ if (retryTimeoutRef.current) clearTimeout(retryTimeoutRef.current);
362
+ if (redirectTimeoutRef.current) clearTimeout(redirectTimeoutRef.current);
363
+ };
364
+ }, [handleCallback]);
365
+
366
+ const handleRetry = useCallback(() => {
367
+ const persistedState = getConsentState();
368
+ if (persistedState?.originalUrl) {
369
+ clearConsentState();
370
+ window.location.href = persistedState.originalUrl;
371
+ } else {
372
+ window.location.href = "/";
373
+ }
374
+ }, []);
375
+
376
+ // Inline styles for the callback page
377
+ const containerStyle: React.CSSProperties = {
378
+ display: "flex",
379
+ alignItems: "center",
380
+ justifyContent: "center",
381
+ minHeight: "100vh",
382
+ backgroundColor: "#f9fafb",
383
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
384
+ padding: "20px",
385
+ boxSizing: "border-box",
386
+ };
387
+
388
+ const cardStyle: React.CSSProperties = {
389
+ backgroundColor: "#ffffff",
390
+ borderRadius: "12px",
391
+ boxShadow: "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)",
392
+ padding: "40px",
393
+ textAlign: "center",
394
+ maxWidth: "400px",
395
+ width: "100%",
396
+ };
397
+
398
+ const spinnerStyle: React.CSSProperties = {
399
+ width: "48px",
400
+ height: "48px",
401
+ border: "4px solid #e5e7eb",
402
+ borderTopColor: "#3b82f6",
403
+ borderRadius: "50%",
404
+ animation: "spin 1s linear infinite",
405
+ margin: "0 auto 24px",
406
+ };
407
+
408
+ const iconStyle: React.CSSProperties = {
409
+ width: "64px",
410
+ height: "64px",
411
+ borderRadius: "50%",
412
+ display: "flex",
413
+ alignItems: "center",
414
+ justifyContent: "center",
415
+ margin: "0 auto 24px",
416
+ };
417
+
418
+ const successIconStyle: React.CSSProperties = {
419
+ ...iconStyle,
420
+ backgroundColor: "#d1fae5",
421
+ color: "#059669",
422
+ };
423
+
424
+ const errorIconStyle: React.CSSProperties = {
425
+ ...iconStyle,
426
+ backgroundColor: "#fee2e2",
427
+ color: "#dc2626",
428
+ };
429
+
430
+ const titleStyle: React.CSSProperties = {
431
+ fontSize: "20px",
432
+ fontWeight: 600,
433
+ color: "#111827",
434
+ margin: "0 0 12px 0",
435
+ };
436
+
437
+ const messageStyle: React.CSSProperties = {
438
+ fontSize: "14px",
439
+ color: "#6b7280",
440
+ marginBottom: "8px",
441
+ lineHeight: 1.5,
442
+ };
443
+
444
+ const errorDetailStyle: React.CSSProperties = {
445
+ fontSize: "13px",
446
+ color: "#dc2626",
447
+ marginTop: "12px",
448
+ padding: "12px",
449
+ backgroundColor: "#fef2f2",
450
+ borderRadius: "6px",
451
+ border: "1px solid #fecaca",
452
+ };
453
+
454
+ const buttonStyle: React.CSSProperties = {
455
+ marginTop: "20px",
456
+ padding: "10px 24px",
457
+ backgroundColor: "#3b82f6",
458
+ color: "#ffffff",
459
+ border: "none",
460
+ borderRadius: "6px",
461
+ fontSize: "14px",
462
+ fontWeight: 500,
463
+ cursor: "pointer",
464
+ transition: "background-color 0.2s",
465
+ };
466
+
467
+ return (
468
+ <div style={containerStyle}>
469
+ <div style={cardStyle}>
470
+ {status === "processing" && (
471
+ <>
472
+ <div style={spinnerStyle} />
473
+ <h2 style={titleStyle}>Processing Verification</h2>
474
+ <p style={messageStyle}>{message}</p>
475
+ <p style={{ ...messageStyle, fontSize: "12px", color: "#9ca3af" }}>
476
+ Please wait while we verify your guardian's identity...
477
+ </p>
478
+ </>
479
+ )}
480
+
481
+ {status === "success" && (
482
+ <>
483
+ <div style={successIconStyle}>
484
+ <svg
485
+ width="32"
486
+ height="32"
487
+ viewBox="0 0 24 24"
488
+ fill="none"
489
+ stroke="currentColor"
490
+ strokeWidth="2"
491
+ strokeLinecap="round"
492
+ strokeLinejoin="round"
493
+ >
494
+ <polyline points="20 6 9 17 4 12" />
495
+ </svg>
496
+ </div>
497
+ <h2 style={titleStyle}>Verification Successful</h2>
498
+ <p style={messageStyle}>{message}</p>
499
+ </>
500
+ )}
501
+
502
+ {status === "error" && (
503
+ <>
504
+ <div style={errorIconStyle}>
505
+ <svg
506
+ width="32"
507
+ height="32"
508
+ viewBox="0 0 24 24"
509
+ fill="none"
510
+ stroke="currentColor"
511
+ strokeWidth="2"
512
+ strokeLinecap="round"
513
+ strokeLinejoin="round"
514
+ >
515
+ <line x1="18" y1="6" x2="6" y2="18" />
516
+ <line x1="6" y1="6" x2="18" y2="18" />
517
+ </svg>
518
+ </div>
519
+ <h2 style={titleStyle}>Verification Failed</h2>
520
+ <p style={messageStyle}>{message}</p>
521
+ {errorDetail && <p style={errorDetailStyle}>{errorDetail}</p>}
522
+ <button
523
+ className="redacto-dl-retry-btn"
524
+ style={buttonStyle}
525
+ onClick={handleRetry}
526
+ >
527
+ Try Again
528
+ </button>
529
+ </>
530
+ )}
531
+ </div>
532
+ </div>
533
+ );
534
+ };
535
+
536
+ export default DigiLockerCallback;