@redacto.io/consent-sdk-react 3.4.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.
- package/.turbo/turbo-build.log +21 -0
- package/CHANGELOG.md +12 -0
- package/dist/index.d.mts +49 -2
- package/dist/index.d.ts +49 -2
- package/dist/index.js +1806 -345
- package/dist/index.mjs +1805 -345
- package/package.json +1 -1
- package/src/RedactoNoticeConsent/RedactoNoticeConsent.test.tsx +201 -118
- package/src/RedactoNoticeConsent/RedactoNoticeConsent.tsx +1192 -93
- package/src/RedactoNoticeConsent/api/index.ts +335 -246
- package/src/RedactoNoticeConsent/api/types.ts +138 -41
- package/src/RedactoNoticeConsent/components/DigiLockerCallback.tsx +536 -0
- package/src/RedactoNoticeConsent/components/index.ts +7 -0
- package/src/RedactoNoticeConsent/injectStyles.ts +11 -0
- package/src/RedactoNoticeConsent/services/storage.ts +122 -0
- package/src/RedactoNoticeConsent/types.ts +2 -0
- package/src/RedactoNoticeConsentInline/api/index.ts +26 -64
- package/src/RedactoNoticeConsentInline/api/types.ts +0 -25
- package/src/index.ts +10 -0
- package/src/utils/isLegacyApi.ts +0 -12
|
@@ -4,18 +4,17 @@ import type {
|
|
|
4
4
|
ConsentEventPayload,
|
|
5
5
|
FetchConsentContentParams,
|
|
6
6
|
FetchTTSAudioUrlsParams,
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
InitiateGuardianVerificationParams,
|
|
8
|
+
InitiateGuardianVerificationResponse,
|
|
9
9
|
RedactoJwtPayload,
|
|
10
10
|
SubmitConsentEventParams,
|
|
11
|
-
SubmitGuardianInfoParams,
|
|
12
11
|
TTSAudioUrlsResponse,
|
|
12
|
+
VerifyGuardianStatusParams,
|
|
13
|
+
VerifyGuardianStatusResponse,
|
|
13
14
|
} from "./types";
|
|
14
|
-
import { isLegacyApi } from "../../utils/isLegacyApi";
|
|
15
15
|
|
|
16
16
|
const BASE_URL = "https://api.redacto.io/consent";
|
|
17
17
|
|
|
18
|
-
|
|
19
18
|
// Simple in-memory cache for API responses to reduce calls
|
|
20
19
|
const apiCache = new Map<string, { data: any; timestamp: number; ttl: number }>();
|
|
21
20
|
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
|
@@ -60,90 +59,82 @@ export const fetchConsentContent = async ({
|
|
|
60
59
|
specific_uuid,
|
|
61
60
|
validate_against = "all",
|
|
62
61
|
include_fully_consented_data = false,
|
|
63
|
-
signal,
|
|
62
|
+
signal,
|
|
64
63
|
}: FetchConsentContentParams & { signal?: AbortSignal }): Promise<ConsentContent> => {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
throw new Error("noticeId and accessToken are required");
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const decodedToken = decodeTokenSafely(accessToken);
|
|
72
|
-
if (!decodedToken) {
|
|
73
|
-
throw new Error("Invalid access token");
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const { organisation_uuid: ORGANISATION_UUID, workspace_uuid: WORKSPACE_UUID } = decodedToken;
|
|
77
|
-
if (!ORGANISATION_UUID || !WORKSPACE_UUID) {
|
|
78
|
-
throw new Error("Invalid token: missing organization or workspace UUID");
|
|
79
|
-
}
|
|
64
|
+
if (!noticeId || !accessToken || !validate_against) {
|
|
65
|
+
throw new Error("noticeId and accessToken are required");
|
|
66
|
+
}
|
|
80
67
|
|
|
81
|
-
|
|
68
|
+
const decodedToken = decodeTokenSafely(accessToken);
|
|
69
|
+
if (!decodedToken) {
|
|
70
|
+
throw new Error("Invalid access token");
|
|
71
|
+
}
|
|
82
72
|
|
|
83
|
-
|
|
84
|
-
|
|
73
|
+
const { organisation_uuid: ORGANISATION_UUID, workspace_uuid: WORKSPACE_UUID } = decodedToken;
|
|
74
|
+
if (!ORGANISATION_UUID || !WORKSPACE_UUID) {
|
|
75
|
+
throw new Error("Invalid token: missing organization or workspace UUID");
|
|
76
|
+
}
|
|
85
77
|
|
|
86
|
-
|
|
87
|
-
if (validate_against === "all" && !specific_uuid) {
|
|
88
|
-
const cachedData = getCachedData(cacheKey);
|
|
89
|
-
if (cachedData) {
|
|
90
|
-
return cachedData;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
78
|
+
const apiBaseUrl = baseUrl || BASE_URL;
|
|
93
79
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
);
|
|
80
|
+
// Create cache key for this request - include accessToken to ensure different users get different cache
|
|
81
|
+
const cacheKey = `${accessToken}-${noticeId}-${validate_against}-${language}-${specific_uuid || ''}-${include_fully_consented_data}`;
|
|
97
82
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (include_fully_consented_data) {
|
|
103
|
-
url.searchParams.append("include_fully_consented_data", "true");
|
|
83
|
+
if (validate_against === "all" && !specific_uuid) {
|
|
84
|
+
const cachedData = getCachedData(cacheKey);
|
|
85
|
+
if (cachedData) {
|
|
86
|
+
return cachedData;
|
|
104
87
|
}
|
|
88
|
+
}
|
|
105
89
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
Authorization: `Bearer ${accessToken}`,
|
|
110
|
-
"Accept-Language": language,
|
|
111
|
-
"Content-Type": "application/json",
|
|
112
|
-
},
|
|
113
|
-
signal, // Add abort signal for request cancellation
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
if (!response.ok) {
|
|
117
|
-
let errorMessage = `Failed to fetch consent content: ${response.status} ${response.statusText}`;
|
|
90
|
+
const url = new URL(
|
|
91
|
+
`${apiBaseUrl}/public/organisations/${ORGANISATION_UUID}/workspaces/${WORKSPACE_UUID}/notices/${noticeId}`
|
|
92
|
+
);
|
|
118
93
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
errorMessage = "User has already provided consent";
|
|
127
|
-
} else if (response.status >= 500) {
|
|
128
|
-
errorMessage = "Server error: Please try again later";
|
|
129
|
-
}
|
|
94
|
+
if (specific_uuid) {
|
|
95
|
+
url.searchParams.append("specific_uuid", specific_uuid);
|
|
96
|
+
}
|
|
97
|
+
url.searchParams.append("validate_against", validate_against);
|
|
98
|
+
if (include_fully_consented_data) {
|
|
99
|
+
url.searchParams.append("include_fully_consented_data", "true");
|
|
100
|
+
}
|
|
130
101
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
102
|
+
const response = await fetch(url.toString(), {
|
|
103
|
+
method: "GET",
|
|
104
|
+
headers: {
|
|
105
|
+
Authorization: `Bearer ${accessToken}`,
|
|
106
|
+
"Accept-Language": language,
|
|
107
|
+
},
|
|
108
|
+
signal,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
if (!response.ok) {
|
|
112
|
+
let errorMessage = `Failed to fetch consent content: ${response.status} ${response.statusText}`;
|
|
113
|
+
|
|
114
|
+
if (response.status === 401) {
|
|
115
|
+
errorMessage = "Unauthorized: Invalid or expired token";
|
|
116
|
+
} else if (response.status === 403) {
|
|
117
|
+
errorMessage = "Forbidden: Access denied";
|
|
118
|
+
} else if (response.status === 404) {
|
|
119
|
+
errorMessage = "Notice not found";
|
|
120
|
+
} else if (response.status === 409) {
|
|
121
|
+
errorMessage = "User has already provided consent";
|
|
122
|
+
} else if (response.status >= 500) {
|
|
123
|
+
errorMessage = "Server error: Please try again later";
|
|
134
124
|
}
|
|
135
125
|
|
|
136
|
-
const
|
|
126
|
+
const error = new Error(errorMessage) as Error & { status?: number };
|
|
127
|
+
error.status = response.status;
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
137
130
|
|
|
138
|
-
|
|
139
|
-
if (validate_against === "all" && !specific_uuid) {
|
|
140
|
-
setCachedData(cacheKey, data);
|
|
141
|
-
}
|
|
131
|
+
const data: ConsentContent = await response.json();
|
|
142
132
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
throw error;
|
|
133
|
+
if (validate_against === "all" && !specific_uuid) {
|
|
134
|
+
setCachedData(cacheKey, data);
|
|
146
135
|
}
|
|
136
|
+
|
|
137
|
+
return data;
|
|
147
138
|
};
|
|
148
139
|
|
|
149
140
|
export const submitConsentEvent = async ({
|
|
@@ -153,156 +144,190 @@ export const submitConsentEvent = async ({
|
|
|
153
144
|
purposes,
|
|
154
145
|
declined,
|
|
155
146
|
meta_data,
|
|
156
|
-
|
|
147
|
+
guardianVerificationReference,
|
|
148
|
+
selfDeclaredAdult,
|
|
149
|
+
signal,
|
|
157
150
|
}: SubmitConsentEventParams & { signal?: AbortSignal }): Promise<void> => {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
throw new Error("noticeUuid, accessToken, and purposes array are required");
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const decodedToken = decodeTokenSafely(accessToken);
|
|
165
|
-
if (!decodedToken) {
|
|
166
|
-
throw new Error("Invalid access token");
|
|
167
|
-
}
|
|
151
|
+
if (!noticeUuid || !accessToken || !Array.isArray(purposes)) {
|
|
152
|
+
throw new Error("noticeUuid, accessToken, and purposes array are required");
|
|
153
|
+
}
|
|
168
154
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
155
|
+
const decodedToken = decodeTokenSafely(accessToken);
|
|
156
|
+
if (!decodedToken) {
|
|
157
|
+
throw new Error("Invalid access token");
|
|
158
|
+
}
|
|
173
159
|
|
|
174
|
-
|
|
160
|
+
const { organisation_uuid: ORGANISATION_UUID, workspace_uuid: WORKSPACE_UUID } = decodedToken;
|
|
161
|
+
if (!ORGANISATION_UUID || !WORKSPACE_UUID) {
|
|
162
|
+
throw new Error("Invalid token: missing organization or workspace UUID");
|
|
163
|
+
}
|
|
175
164
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if (isLegacyApi(apiBaseUrl)) {
|
|
179
|
-
// @deprecated Legacy payload for production (.io) backend
|
|
180
|
-
const primaryEmail = typeof window !== "undefined" ? localStorage.getItem("userEmail") : null;
|
|
181
|
-
payload = {
|
|
182
|
-
primary_email: primaryEmail || undefined,
|
|
183
|
-
notice_uuid: noticeUuid,
|
|
184
|
-
source: "WEB",
|
|
185
|
-
declined: Boolean(declined),
|
|
186
|
-
consents: {
|
|
187
|
-
purposes: purposes.map((purpose) => {
|
|
188
|
-
if (!purpose.uuid) {
|
|
189
|
-
throw new Error(`Invalid purpose data: missing uuid`);
|
|
190
|
-
}
|
|
191
|
-
return {
|
|
192
|
-
uuid: purpose.uuid,
|
|
193
|
-
name: purpose.name,
|
|
194
|
-
description: purpose.description || "",
|
|
195
|
-
industries: purpose.industries || "",
|
|
196
|
-
selected: Boolean(purpose.selected),
|
|
197
|
-
data_elements: (purpose.data_elements || []).map((element) => ({
|
|
198
|
-
uuid: element.uuid,
|
|
199
|
-
name: element.name,
|
|
200
|
-
enabled: Boolean(element.enabled),
|
|
201
|
-
required: Boolean(element.required),
|
|
202
|
-
selected: element.required ? true : Boolean(element.selected),
|
|
203
|
-
})),
|
|
204
|
-
};
|
|
205
|
-
}),
|
|
206
|
-
},
|
|
207
|
-
meta_data: meta_data ? { specific_uuid: meta_data.specific_uuid } : undefined,
|
|
208
|
-
};
|
|
209
|
-
} else {
|
|
210
|
-
// New payload for staging (.tech) / localhost
|
|
211
|
-
const validatedPurposes = purposes.map((purpose) => {
|
|
212
|
-
if (!purpose.uuid) {
|
|
213
|
-
throw new Error(`Invalid purpose data: missing uuid`);
|
|
214
|
-
}
|
|
165
|
+
const apiBaseUrl = baseUrl || BASE_URL;
|
|
215
166
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
.map((element) => ({
|
|
220
|
-
uuid: element.uuid,
|
|
221
|
-
selected: element.required ? true : Boolean(element.selected),
|
|
222
|
-
}));
|
|
223
|
-
|
|
224
|
-
return {
|
|
225
|
-
purpose_uuid: purpose.uuid,
|
|
226
|
-
selected: Boolean(purpose.selected),
|
|
227
|
-
data_elements: dataElements.length > 0 ? dataElements : undefined,
|
|
228
|
-
};
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
payload = {
|
|
232
|
-
notice_uuid: noticeUuid,
|
|
233
|
-
purposes: validatedPurposes,
|
|
234
|
-
select_all_mandatory: false,
|
|
235
|
-
source: "WEB",
|
|
236
|
-
declined: Boolean(declined),
|
|
237
|
-
meta_data: meta_data ? { specific_uuid: meta_data.specific_uuid } : undefined,
|
|
238
|
-
};
|
|
167
|
+
const validatedPurposes = purposes.map((purpose) => {
|
|
168
|
+
if (!purpose.uuid) {
|
|
169
|
+
throw new Error(`Invalid purpose data: missing uuid`);
|
|
239
170
|
}
|
|
240
171
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
172
|
+
// Only include enabled data elements (disabled elements are not selectable)
|
|
173
|
+
const dataElements = (purpose.data_elements || [])
|
|
174
|
+
.filter((element) => element.enabled)
|
|
175
|
+
.map((element) => ({
|
|
176
|
+
uuid: element.uuid,
|
|
177
|
+
selected: element.required ? true : Boolean(element.selected),
|
|
178
|
+
}));
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
purpose_uuid: purpose.uuid,
|
|
182
|
+
selected: Boolean(purpose.selected),
|
|
183
|
+
data_elements: dataElements.length > 0 ? dataElements : undefined,
|
|
184
|
+
};
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const payload: ConsentEventPayload = {
|
|
188
|
+
notice_uuid: noticeUuid,
|
|
189
|
+
purposes: validatedPurposes,
|
|
190
|
+
select_all_mandatory: false,
|
|
191
|
+
source: "WEB",
|
|
192
|
+
declined: Boolean(declined),
|
|
193
|
+
meta_data: meta_data ? { specific_uuid: meta_data.specific_uuid, minor_age: meta_data.minor_age } : undefined,
|
|
194
|
+
guardian_verification_reference: guardianVerificationReference,
|
|
195
|
+
self_declared_adult: selfDeclaredAdult,
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const response = await fetch(
|
|
199
|
+
`${apiBaseUrl}/public/organisations/${ORGANISATION_UUID}/workspaces/${WORKSPACE_UUID}/submit-consent`,
|
|
200
|
+
{
|
|
201
|
+
method: "POST",
|
|
202
|
+
headers: {
|
|
203
|
+
Authorization: `Bearer ${accessToken}`,
|
|
204
|
+
"Content-Type": "application/json",
|
|
205
|
+
},
|
|
206
|
+
body: JSON.stringify(payload),
|
|
207
|
+
signal,
|
|
208
|
+
}
|
|
209
|
+
);
|
|
253
210
|
|
|
254
|
-
|
|
255
|
-
|
|
211
|
+
if (!response.ok) {
|
|
212
|
+
let errorMessage = `Failed to submit consent: ${response.status} ${response.statusText}`;
|
|
256
213
|
|
|
257
|
-
|
|
214
|
+
if (response.status === 400) {
|
|
215
|
+
try {
|
|
216
|
+
const errorData = await response.json();
|
|
217
|
+
errorMessage = errorData.message || "Invalid request data";
|
|
218
|
+
} catch {
|
|
258
219
|
errorMessage = "Invalid request data";
|
|
259
|
-
} else if (response.status === 401) {
|
|
260
|
-
errorMessage = "Unauthorized: Invalid or expired token";
|
|
261
|
-
} else if (response.status === 403) {
|
|
262
|
-
errorMessage = "Forbidden: Access denied";
|
|
263
|
-
} else if (response.status === 404) {
|
|
264
|
-
errorMessage = "Notice not found";
|
|
265
|
-
} else if (response.status >= 500) {
|
|
266
|
-
errorMessage = "Server error: Please try again later";
|
|
267
220
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
221
|
+
} else if (response.status === 401) {
|
|
222
|
+
errorMessage = "Unauthorized: Invalid or expired token";
|
|
223
|
+
} else if (response.status === 403) {
|
|
224
|
+
errorMessage = "Forbidden: Access denied";
|
|
225
|
+
} else if (response.status === 404) {
|
|
226
|
+
errorMessage = "Notice not found";
|
|
227
|
+
} else if (response.status >= 500) {
|
|
228
|
+
errorMessage = "Server error: Please try again later";
|
|
272
229
|
}
|
|
273
230
|
|
|
274
|
-
|
|
275
|
-
|
|
231
|
+
const error = new Error(errorMessage) as Error & { status?: number };
|
|
232
|
+
error.status = response.status;
|
|
233
|
+
throw error;
|
|
234
|
+
}
|
|
276
235
|
|
|
277
|
-
|
|
236
|
+
// Clear any cached consent content since user preferences have changed
|
|
237
|
+
apiCache.clear();
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
export const fetchTTSAudioUrls = async ({
|
|
241
|
+
accessToken,
|
|
242
|
+
baseUrl,
|
|
243
|
+
noticeUuid,
|
|
244
|
+
language,
|
|
245
|
+
signal,
|
|
246
|
+
}: FetchTTSAudioUrlsParams & { signal?: AbortSignal }): Promise<TTSAudioUrlsResponse> => {
|
|
247
|
+
if (!accessToken || !noticeUuid || !language) {
|
|
248
|
+
throw new Error("accessToken, noticeUuid, and language are required");
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const decodedToken = decodeTokenSafely(accessToken);
|
|
252
|
+
if (!decodedToken) {
|
|
253
|
+
throw new Error("Invalid access token");
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const { organisation_uuid: ORGANISATION_UUID, workspace_uuid: WORKSPACE_UUID } = decodedToken;
|
|
257
|
+
if (!ORGANISATION_UUID || !WORKSPACE_UUID) {
|
|
258
|
+
throw new Error("Invalid token: missing organization or workspace UUID");
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const apiBaseUrl = baseUrl || BASE_URL;
|
|
262
|
+
|
|
263
|
+
const url = `${apiBaseUrl}/public/organisations/${ORGANISATION_UUID}/workspaces/${WORKSPACE_UUID}/notices/${noticeUuid}/audio/${language}`;
|
|
264
|
+
|
|
265
|
+
const response = await fetch(url, {
|
|
266
|
+
method: "GET",
|
|
267
|
+
headers: {
|
|
268
|
+
Authorization: `Bearer ${accessToken}`,
|
|
269
|
+
Accept: "application/json",
|
|
270
|
+
},
|
|
271
|
+
signal,
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
if (!response.ok) {
|
|
275
|
+
let errorMessage = `Failed to fetch TTS audio URLs: ${response.status} ${response.statusText}`;
|
|
276
|
+
|
|
277
|
+
if (response.status === 401) {
|
|
278
|
+
errorMessage = "Unauthorized: Invalid or expired token";
|
|
279
|
+
} else if (response.status === 403) {
|
|
280
|
+
errorMessage = "Forbidden: Access denied";
|
|
281
|
+
} else if (response.status === 404) {
|
|
282
|
+
errorMessage = "Notice or audio not found";
|
|
283
|
+
} else if (response.status >= 500) {
|
|
284
|
+
errorMessage = "Server error: Please try again later";
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const error = new Error(errorMessage) as Error & { status?: number };
|
|
288
|
+
error.status = response.status;
|
|
278
289
|
throw error;
|
|
279
290
|
}
|
|
291
|
+
|
|
292
|
+
const data: TTSAudioUrlsResponse = await response.json();
|
|
293
|
+
return data;
|
|
280
294
|
};
|
|
281
295
|
|
|
282
|
-
|
|
296
|
+
// =============================================================================
|
|
297
|
+
// Guardian Verification API Functions
|
|
298
|
+
// =============================================================================
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Initiate guardian verification via DigiLocker
|
|
302
|
+
* POST /guardian/initiate-verification
|
|
303
|
+
*
|
|
304
|
+
* This combines guardian details submission and DigiLocker initiation in one call.
|
|
305
|
+
*/
|
|
306
|
+
export const initiateGuardianVerification = async ({
|
|
283
307
|
accessToken,
|
|
284
308
|
baseUrl,
|
|
285
309
|
guardianName,
|
|
286
310
|
guardianContact,
|
|
287
311
|
guardianRelationship,
|
|
288
|
-
|
|
289
|
-
|
|
312
|
+
frontendCallbackUrl,
|
|
313
|
+
metadata,
|
|
314
|
+
signal,
|
|
315
|
+
}: InitiateGuardianVerificationParams): Promise<InitiateGuardianVerificationResponse> => {
|
|
290
316
|
try {
|
|
291
|
-
// Validate required parameters
|
|
292
317
|
if (!accessToken) {
|
|
293
318
|
throw new Error("accessToken is required");
|
|
294
319
|
}
|
|
295
320
|
|
|
296
321
|
if (!guardianName?.trim()) {
|
|
297
|
-
throw new Error("
|
|
322
|
+
throw new Error("Guardian name is required");
|
|
298
323
|
}
|
|
299
324
|
|
|
300
325
|
if (!guardianContact?.trim()) {
|
|
301
|
-
throw new Error("
|
|
326
|
+
throw new Error("Guardian contact is required");
|
|
302
327
|
}
|
|
303
328
|
|
|
304
329
|
if (!guardianRelationship?.trim()) {
|
|
305
|
-
throw new Error("
|
|
330
|
+
throw new Error("Guardian relationship is required");
|
|
306
331
|
}
|
|
307
332
|
|
|
308
333
|
const decodedToken = decodeTokenSafely(accessToken);
|
|
@@ -315,16 +340,30 @@ export const submitGuardianInfo = async ({
|
|
|
315
340
|
throw new Error("Invalid token: missing organization or workspace UUID");
|
|
316
341
|
}
|
|
317
342
|
|
|
318
|
-
const
|
|
343
|
+
const apiBaseUrl = baseUrl || BASE_URL;
|
|
344
|
+
|
|
345
|
+
const payload: {
|
|
346
|
+
guardian_name: string;
|
|
347
|
+
guardian_contact: string;
|
|
348
|
+
guardian_relationship: string;
|
|
349
|
+
frontend_callback_url?: string;
|
|
350
|
+
metadata?: Record<string, unknown>;
|
|
351
|
+
} = {
|
|
319
352
|
guardian_name: guardianName.trim(),
|
|
320
353
|
guardian_contact: guardianContact.trim(),
|
|
321
354
|
guardian_relationship: guardianRelationship.trim(),
|
|
322
355
|
};
|
|
323
356
|
|
|
324
|
-
|
|
357
|
+
if (frontendCallbackUrl) {
|
|
358
|
+
payload.frontend_callback_url = frontendCallbackUrl;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (metadata) {
|
|
362
|
+
payload.metadata = metadata;
|
|
363
|
+
}
|
|
325
364
|
|
|
326
365
|
const response = await fetch(
|
|
327
|
-
`${apiBaseUrl}/public/organisations/${ORGANISATION_UUID}/workspaces/${WORKSPACE_UUID}/guardian-
|
|
366
|
+
`${apiBaseUrl}/public/organisations/${ORGANISATION_UUID}/workspaces/${WORKSPACE_UUID}/guardian/initiate-verification`,
|
|
328
367
|
{
|
|
329
368
|
method: "POST",
|
|
330
369
|
headers: {
|
|
@@ -332,27 +371,29 @@ export const submitGuardianInfo = async ({
|
|
|
332
371
|
"Content-Type": "application/json",
|
|
333
372
|
},
|
|
334
373
|
body: JSON.stringify(payload),
|
|
335
|
-
signal,
|
|
374
|
+
signal,
|
|
336
375
|
}
|
|
337
376
|
);
|
|
338
377
|
|
|
339
378
|
if (!response.ok) {
|
|
340
|
-
let errorMessage = `Failed to
|
|
379
|
+
let errorMessage = `Failed to initiate verification: ${response.status} ${response.statusText}`;
|
|
341
380
|
|
|
342
381
|
if (response.status === 400) {
|
|
343
|
-
|
|
382
|
+
try {
|
|
383
|
+
const errorData = await response.json();
|
|
384
|
+
errorMessage = errorData.message || "Invalid request data";
|
|
385
|
+
} catch {
|
|
386
|
+
errorMessage = "Invalid request data";
|
|
387
|
+
}
|
|
344
388
|
} else if (response.status === 401) {
|
|
345
389
|
errorMessage = "Unauthorized: Invalid or expired token";
|
|
346
|
-
} else if (response.status === 403) {
|
|
347
|
-
errorMessage = "Forbidden: Access denied";
|
|
348
390
|
} else if (response.status === 404) {
|
|
349
391
|
errorMessage = "Organization or workspace not found";
|
|
350
392
|
} else if (response.status === 422) {
|
|
351
|
-
// Handle validation errors from the server
|
|
352
393
|
try {
|
|
353
394
|
const errorData = await response.json();
|
|
354
|
-
errorMessage = errorData.message || "Validation error";
|
|
355
|
-
} catch
|
|
395
|
+
errorMessage = errorData.message || "Validation error: Please check your input";
|
|
396
|
+
} catch {
|
|
356
397
|
errorMessage = "Validation error: Please check your input";
|
|
357
398
|
}
|
|
358
399
|
} else if (response.status >= 500) {
|
|
@@ -364,72 +405,120 @@ export const submitGuardianInfo = async ({
|
|
|
364
405
|
throw error;
|
|
365
406
|
}
|
|
366
407
|
|
|
408
|
+
const responseData = await response.json();
|
|
409
|
+
|
|
410
|
+
// Handle response with detail wrapper (backend format: { code, status, detail: {...} })
|
|
411
|
+
const detail = responseData.detail || responseData;
|
|
412
|
+
|
|
413
|
+
// Already verified — skip DigiLocker entirely
|
|
414
|
+
if (detail.already_verified) {
|
|
415
|
+
return {
|
|
416
|
+
success: true,
|
|
417
|
+
guardian_info_uuid: detail.guardian_info_uuid,
|
|
418
|
+
already_verified: true,
|
|
419
|
+
verification_reference: detail.verification_reference,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// New verification flow — needs DigiLocker
|
|
424
|
+
if (!detail.session_token || !detail.digilocker_redirect_url) {
|
|
425
|
+
throw new Error("Invalid response: missing session_token or digilocker_redirect_url");
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return {
|
|
429
|
+
success: detail.success ?? true,
|
|
430
|
+
guardian_info_uuid: detail.guardian_info_uuid,
|
|
431
|
+
already_verified: false,
|
|
432
|
+
session_token: detail.session_token,
|
|
433
|
+
digilocker_redirect_url: detail.digilocker_redirect_url,
|
|
434
|
+
expires_at: detail.expires_at,
|
|
435
|
+
};
|
|
367
436
|
} catch (error) {
|
|
368
|
-
|
|
369
|
-
console.error("Error submitting guardian info:", error instanceof Error ? error.message : error);
|
|
437
|
+
console.error("Error initiating guardian verification:", error instanceof Error ? error.message : error);
|
|
370
438
|
throw error;
|
|
371
439
|
}
|
|
372
440
|
};
|
|
373
441
|
|
|
374
|
-
|
|
442
|
+
/**
|
|
443
|
+
* Check guardian verification status
|
|
444
|
+
* POST /guardian/verify-status
|
|
445
|
+
*
|
|
446
|
+
* Poll this endpoint to check if DigiLocker verification is complete.
|
|
447
|
+
* Returns verification_reference when status is "verified".
|
|
448
|
+
*/
|
|
449
|
+
export const verifyGuardianStatus = async ({
|
|
375
450
|
accessToken,
|
|
376
451
|
baseUrl,
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
if (!accessToken || !noticeUuid || !language) {
|
|
384
|
-
throw new Error("accessToken, noticeUuid, and language are required");
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
const decodedToken = decodeTokenSafely(accessToken);
|
|
388
|
-
if (!decodedToken) {
|
|
389
|
-
throw new Error("Invalid access token");
|
|
390
|
-
}
|
|
452
|
+
sessionToken,
|
|
453
|
+
signal,
|
|
454
|
+
}: VerifyGuardianStatusParams): Promise<VerifyGuardianStatusResponse> => {
|
|
455
|
+
if (!accessToken || !sessionToken) {
|
|
456
|
+
throw new Error("accessToken and sessionToken are required");
|
|
457
|
+
}
|
|
391
458
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
459
|
+
const decodedToken = decodeTokenSafely(accessToken);
|
|
460
|
+
if (!decodedToken) {
|
|
461
|
+
throw new Error("Invalid access token");
|
|
462
|
+
}
|
|
396
463
|
|
|
397
|
-
|
|
464
|
+
const { organisation_uuid: ORGANISATION_UUID, workspace_uuid: WORKSPACE_UUID } = decodedToken;
|
|
465
|
+
if (!ORGANISATION_UUID || !WORKSPACE_UUID) {
|
|
466
|
+
throw new Error("Invalid token: missing organization or workspace UUID");
|
|
467
|
+
}
|
|
398
468
|
|
|
399
|
-
|
|
469
|
+
const apiBaseUrl = baseUrl || BASE_URL;
|
|
400
470
|
|
|
401
|
-
|
|
402
|
-
|
|
471
|
+
const response = await fetch(
|
|
472
|
+
`${apiBaseUrl}/public/organisations/${ORGANISATION_UUID}/workspaces/${WORKSPACE_UUID}/guardian/verify-status`,
|
|
473
|
+
{
|
|
474
|
+
method: "POST",
|
|
403
475
|
headers: {
|
|
404
476
|
Authorization: `Bearer ${accessToken}`,
|
|
405
|
-
Accept: "application/json",
|
|
406
477
|
"Content-Type": "application/json",
|
|
407
478
|
},
|
|
408
|
-
|
|
409
|
-
|
|
479
|
+
body: JSON.stringify({
|
|
480
|
+
session_token: sessionToken,
|
|
481
|
+
}),
|
|
482
|
+
signal,
|
|
483
|
+
}
|
|
484
|
+
);
|
|
410
485
|
|
|
411
|
-
|
|
412
|
-
|
|
486
|
+
if (!response.ok) {
|
|
487
|
+
let errorMessage = `Failed to check verification status: ${response.status} ${response.statusText}`;
|
|
413
488
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
errorMessage = "
|
|
418
|
-
}
|
|
419
|
-
errorMessage = "
|
|
420
|
-
} else if (response.status >= 500) {
|
|
421
|
-
errorMessage = "Server error: Please try again later";
|
|
489
|
+
if (response.status === 400) {
|
|
490
|
+
try {
|
|
491
|
+
const errorData = await response.json();
|
|
492
|
+
errorMessage = errorData.message || errorData.detail?.message || "Invalid session token";
|
|
493
|
+
} catch {
|
|
494
|
+
errorMessage = "Invalid session token";
|
|
422
495
|
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
496
|
+
} else if (response.status === 401) {
|
|
497
|
+
errorMessage = "Unauthorized: Invalid or expired token";
|
|
498
|
+
} else if (response.status === 404) {
|
|
499
|
+
errorMessage = "Verification session not found or expired. Please restart the verification process.";
|
|
500
|
+
} else if (response.status === 410) {
|
|
501
|
+
errorMessage = "Verification session has expired. Please restart the verification process.";
|
|
502
|
+
} else if (response.status >= 500) {
|
|
503
|
+
errorMessage = "Server error: Please try again later";
|
|
427
504
|
}
|
|
428
505
|
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
return data;
|
|
432
|
-
} catch (error) {
|
|
506
|
+
const error = new Error(errorMessage) as Error & { status?: number };
|
|
507
|
+
error.status = response.status;
|
|
433
508
|
throw error;
|
|
434
509
|
}
|
|
510
|
+
|
|
511
|
+
const responseData = await response.json();
|
|
512
|
+
const detail = responseData.detail || responseData;
|
|
513
|
+
|
|
514
|
+
return {
|
|
515
|
+
status: detail.status ?? "in_progress",
|
|
516
|
+
guardian_info_uuid: detail.guardian_info_uuid ?? null,
|
|
517
|
+
verification_reference: detail.verification_reference ?? null,
|
|
518
|
+
guardian_details: detail.guardian_details ?? null,
|
|
519
|
+
error: detail.error ?? null,
|
|
520
|
+
error_code: detail.error_code ?? null,
|
|
521
|
+
can_retry: detail.can_retry ?? true,
|
|
522
|
+
};
|
|
435
523
|
};
|
|
524
|
+
|