astra-sdk-web 1.1.12 → 1.1.14
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/README.md +179 -179
- package/dist/astra-sdk.cjs.js +2 -2
- package/dist/astra-sdk.cjs.js.map +1 -1
- package/dist/astra-sdk.css +42 -0
- package/dist/astra-sdk.css.map +1 -1
- package/dist/astra-sdk.es.js +2 -2
- package/dist/astra-sdk.es.js.map +1 -1
- package/dist/components.cjs.js +2 -2
- package/dist/components.cjs.js.map +1 -1
- package/dist/components.css +42 -0
- package/dist/components.css.map +1 -1
- package/dist/components.es.js +2 -2
- package/dist/components.es.js.map +1 -1
- package/package.json +74 -74
- package/src/App.tsx +17 -17
- package/src/components/KycFlow.tsx +1 -1
- package/src/components/MobileKycPage.tsx +1122 -0
- package/src/components/kycModal.tsx +551 -0
- package/src/index.css +22 -18
- package/src/main.tsx +10 -10
- package/src/pages/QRCodePage.tsx +1 -1
- package/dist/.htaccess +0 -9
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { toast } from "react-toastify";
|
|
3
|
+
import VerificationSDK from "../VerificationSDK";
|
|
4
|
+
import "./kycModal.css";
|
|
5
|
+
|
|
6
|
+
// Simple QR Code generator using canvas
|
|
7
|
+
const generateQRCode = (text: string, size: number = 120): string => {
|
|
8
|
+
const canvas = document.createElement('canvas');
|
|
9
|
+
const ctx = canvas.getContext('2d');
|
|
10
|
+
if (!ctx) return '';
|
|
11
|
+
|
|
12
|
+
canvas.width = size;
|
|
13
|
+
canvas.height = size;
|
|
14
|
+
|
|
15
|
+
// Simple QR-like pattern (for demo purposes)
|
|
16
|
+
// In production, use a proper QR code library like 'qrcode' or 'qr-scanner'
|
|
17
|
+
ctx.fillStyle = '#000000';
|
|
18
|
+
ctx.fillRect(0, 0, size, size);
|
|
19
|
+
|
|
20
|
+
// Create a simple pattern that looks like a QR code
|
|
21
|
+
const moduleSize = size / 25;
|
|
22
|
+
for (let i = 0; i < 25; i++) {
|
|
23
|
+
for (let j = 0; j < 25; j++) {
|
|
24
|
+
if ((i + j) % 3 === 0 || (i * j) % 7 === 0) {
|
|
25
|
+
ctx.fillStyle = '#ffffff';
|
|
26
|
+
ctx.fillRect(i * moduleSize, j * moduleSize, moduleSize, moduleSize);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Add corner markers
|
|
32
|
+
ctx.fillStyle = '#000000';
|
|
33
|
+
ctx.fillRect(0, 0, moduleSize * 7, moduleSize * 7);
|
|
34
|
+
ctx.fillRect(0, 0, moduleSize * 3, moduleSize * 3);
|
|
35
|
+
ctx.fillRect(moduleSize * 4, 0, moduleSize * 3, moduleSize * 3);
|
|
36
|
+
ctx.fillRect(0, moduleSize * 4, moduleSize * 3, moduleSize * 3);
|
|
37
|
+
|
|
38
|
+
return canvas.toDataURL();
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
interface KycModalClassNames {
|
|
42
|
+
backdrop?: string;
|
|
43
|
+
container?: string;
|
|
44
|
+
closeButton?: string;
|
|
45
|
+
title?: string;
|
|
46
|
+
input?: string;
|
|
47
|
+
statusBlock?: string;
|
|
48
|
+
error?: string;
|
|
49
|
+
loading?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface KycModalStyleOverrides {
|
|
53
|
+
backdrop?: React.CSSProperties;
|
|
54
|
+
container?: React.CSSProperties;
|
|
55
|
+
closeButton?: React.CSSProperties;
|
|
56
|
+
title?: React.CSSProperties;
|
|
57
|
+
input?: React.CSSProperties;
|
|
58
|
+
statusBlock?: React.CSSProperties;
|
|
59
|
+
error?: React.CSSProperties;
|
|
60
|
+
loading?: React.CSSProperties;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface KycModalProps {
|
|
64
|
+
sdk: VerificationSDK;
|
|
65
|
+
onClose: () => void;// e.g., "PASSPORT", "ID_CARD"
|
|
66
|
+
classNames?: KycModalClassNames;
|
|
67
|
+
styles?: KycModalStyleOverrides;
|
|
68
|
+
defaultDocumentType?: string; // Add this
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
type Step = "FACE" | "DOCUMENT" | "STATUS";
|
|
72
|
+
|
|
73
|
+
export const KycModal: React.FC<KycModalProps> = ({ sdk, onClose, classNames, styles, defaultDocumentType = "CNIC" }: KycModalProps) => {
|
|
74
|
+
try { console.log('[KycModal] render'); } catch {}
|
|
75
|
+
// Merge SDK-provided defaults with component props (props win)
|
|
76
|
+
const uiDefaults = sdk.getUiConfig?.() || {};
|
|
77
|
+
const mergedClassNames = { ...(uiDefaults.classNames || {}), ...(classNames || {}) } as KycModalClassNames;
|
|
78
|
+
const mergedStyles = { ...(uiDefaults.styles || {}), ...(styles || {}) } as KycModalStyleOverrides;
|
|
79
|
+
const sessionId = sdk.getSessionId?.() || localStorage.getItem("kyc_session_id") || null;
|
|
80
|
+
const qrFromServer = sdk.getQrCodeUrl?.() || localStorage.getItem("kyc_qrcode_url") || null;
|
|
81
|
+
const mobileUrl = sessionId ? (() => {
|
|
82
|
+
const key = (sdk as any)?.getServerKey?.();
|
|
83
|
+
const origin = (typeof window !== 'undefined' && window.location && window.location.origin) ? window.location.origin : '';
|
|
84
|
+
return `${origin}/mobile-kyc/${sessionId}?mode=face${key ? `&key=${encodeURIComponent(key)}` : ""}`;
|
|
85
|
+
})() : null;
|
|
86
|
+
const showQr = Boolean(qrFromServer || sessionId);
|
|
87
|
+
try { console.log('[KycModal] computed', { sessionId, showQr, hasKey: Boolean((sdk as any)?.getServerKey?.()) }); } catch {}
|
|
88
|
+
const computedQrSrc = qrFromServer || (mobileUrl ? `https://api.qrserver.com/v1/create-qr-code/?size=220x220&data=${encodeURIComponent(mobileUrl)}` : "");
|
|
89
|
+
const [step, setStep] = useState<Step>("FACE");
|
|
90
|
+
const [loading, setLoading] = useState(false);
|
|
91
|
+
const [status, setStatus] = useState<any>(null);
|
|
92
|
+
const videoRef = useRef<HTMLVideoElement | null>(null);
|
|
93
|
+
const streamRef = useRef<MediaStream | null>(null);
|
|
94
|
+
const [cameraReady, setCameraReady] = useState(false);
|
|
95
|
+
const [toastMessage, setToastMessage] = useState<string | null>(null);
|
|
96
|
+
const [docType, setDocType] = useState<string>(defaultDocumentType);
|
|
97
|
+
const [docFileName, setDocFileName] = useState<string>("");
|
|
98
|
+
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
|
99
|
+
|
|
100
|
+
// Map backend next_step/completed_steps to local Step
|
|
101
|
+
const mapStatusToStep = (response: any): Step | null => {
|
|
102
|
+
const data = response?.data ?? response ?? {};
|
|
103
|
+
const nextStepRaw: string | undefined = data?.next_step ?? data?.nextStep;
|
|
104
|
+
const completedSteps: string[] = Array.isArray(data?.completed_steps)
|
|
105
|
+
? data.completed_steps
|
|
106
|
+
: (Array.isArray(data?.completedSteps) ? data.completedSteps : []);
|
|
107
|
+
const norm = (s: string) => String(s || "").toLowerCase().replace(/\s+/g, "_");
|
|
108
|
+
const next = nextStepRaw ? norm(nextStepRaw) : "";
|
|
109
|
+
if (next.includes("face")) return "FACE";
|
|
110
|
+
if (next.includes("doc")) return "DOCUMENT";
|
|
111
|
+
if (next.includes("status") || next.includes("result")) return "STATUS";
|
|
112
|
+
// Fallback using completed steps when next_step is missing
|
|
113
|
+
const cs = completedSteps.map(norm);
|
|
114
|
+
if (cs.includes("face_scan") && !cs.includes("document_upload")) return "DOCUMENT";
|
|
115
|
+
if (cs.includes("face_scan") && cs.includes("document_upload")) return "STATUS";
|
|
116
|
+
return null;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// Extract a human-friendly error from SDK/network errors
|
|
120
|
+
const getDisplayError = (err: unknown): string => {
|
|
121
|
+
const raw = (err && (err as any).message) ? String((err as any).message) : String(err || "");
|
|
122
|
+
// Try to extract JSON payload after a colon
|
|
123
|
+
const afterColon = raw.includes(":") ? raw.split(":").slice(1).join(":").trim() : raw;
|
|
124
|
+
try {
|
|
125
|
+
const parsed = JSON.parse(afterColon);
|
|
126
|
+
if (parsed && typeof parsed.message === "string") return parsed.message;
|
|
127
|
+
} catch {}
|
|
128
|
+
// Pull common phrases (e.g., "Face does not match") if present in raw
|
|
129
|
+
const match = raw.match(/(Face does not match[^\n]*)/i) || raw.match(/(Document[^\n]*failed[^\n]*)/i);
|
|
130
|
+
if (match && match[1]) return match[1];
|
|
131
|
+
// Fallback to trimmed raw message without the generic prefix
|
|
132
|
+
return afterColon.replace(/^document upload failed\s*/i, "").replace(/^face upload failed\s*/i, "").trim() || "Something went wrong";
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const handleRetryDocument = () => {
|
|
136
|
+
setDocFileName("");
|
|
137
|
+
if (fileInputRef.current) {
|
|
138
|
+
fileInputRef.current.value = "";
|
|
139
|
+
fileInputRef.current.click();
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// Auto-hide toast after 3 seconds
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
if (!toastMessage) return;
|
|
146
|
+
const id = setTimeout(() => setToastMessage(null), 3000);
|
|
147
|
+
return () => clearTimeout(id);
|
|
148
|
+
}, [toastMessage]);
|
|
149
|
+
|
|
150
|
+
// Start camera when FACE step shows
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
const startCamera = async () => {
|
|
153
|
+
try {
|
|
154
|
+
const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: "user" }, audio: false });
|
|
155
|
+
if (videoRef.current) {
|
|
156
|
+
videoRef.current.srcObject = stream;
|
|
157
|
+
await videoRef.current.play().catch(() => {});
|
|
158
|
+
}
|
|
159
|
+
streamRef.current = stream;
|
|
160
|
+
setCameraReady(true);
|
|
161
|
+
} catch (e: any) {
|
|
162
|
+
toast.error(e?.message || "Could not access camera");
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
if (step === "FACE") {
|
|
166
|
+
startCamera();
|
|
167
|
+
}
|
|
168
|
+
return () => {
|
|
169
|
+
// Stop camera when leaving screen/component
|
|
170
|
+
if (streamRef.current) {
|
|
171
|
+
streamRef.current.getTracks().forEach((t) => t.stop());
|
|
172
|
+
streamRef.current = null;
|
|
173
|
+
}
|
|
174
|
+
setCameraReady(false);
|
|
175
|
+
};
|
|
176
|
+
}, [step]);
|
|
177
|
+
|
|
178
|
+
// On each step, check session status; if expired, inform and close
|
|
179
|
+
useEffect(() => {
|
|
180
|
+
if (!sessionId) return; // avoid calling status without a session
|
|
181
|
+
let cancelled = false;
|
|
182
|
+
const checkStatus = async () => {
|
|
183
|
+
try {
|
|
184
|
+
const res: any = await sdk.getStatus(sdk?.getServerKey());
|
|
185
|
+
if (cancelled) return;
|
|
186
|
+
// Detect expired by explicit status or heuristics
|
|
187
|
+
const explicitStatus: string | undefined = res?.data?.status ?? res?.status;
|
|
188
|
+
const msg: string | undefined = res?.message;
|
|
189
|
+
const expiredFlag: boolean | undefined = res?.data?.expired ?? res?.expired;
|
|
190
|
+
const expiredAt: string | undefined = res?.data?.expired_at ?? res?.expired_at;
|
|
191
|
+
const expiredByDate = expiredAt ? new Date(expiredAt).getTime() < Date.now() : false;
|
|
192
|
+
const expiredByMsg = typeof msg === "string" && msg.toLowerCase().includes("expired");
|
|
193
|
+
if ((explicitStatus && explicitStatus.toUpperCase() === "EXPIRED") || expiredFlag || expiredByDate || expiredByMsg) {
|
|
194
|
+
setToastMessage("Session expired. Returning to previous screen...");
|
|
195
|
+
setTimeout(() => {
|
|
196
|
+
if (!cancelled) onClose();
|
|
197
|
+
}, 1200);
|
|
198
|
+
}
|
|
199
|
+
// Drive UI using next_step/completed_steps
|
|
200
|
+
const suggested = mapStatusToStep(res);
|
|
201
|
+
if (suggested && suggested !== step) {
|
|
202
|
+
setStep(suggested);
|
|
203
|
+
if (suggested === "STATUS") {
|
|
204
|
+
try {
|
|
205
|
+
const result = await sdk.getResult(sdk?.getServerKey());
|
|
206
|
+
setStatus(result);
|
|
207
|
+
} catch {
|
|
208
|
+
setStatus(res);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
} catch (e: any) {
|
|
213
|
+
// If the server signals expiry via error message
|
|
214
|
+
const emsg = e?.message as string | undefined;
|
|
215
|
+
if (emsg && emsg.toLowerCase().includes("expired")) {
|
|
216
|
+
setToastMessage("Session expired. Returning to previous screen...");
|
|
217
|
+
setTimeout(() => {
|
|
218
|
+
if (!cancelled) onClose();
|
|
219
|
+
}, 1200);
|
|
220
|
+
}
|
|
221
|
+
// else ignore to avoid disrupting other flows
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
checkStatus();
|
|
225
|
+
return () => {
|
|
226
|
+
cancelled = true;
|
|
227
|
+
};
|
|
228
|
+
}, [step, sdk, onClose]);
|
|
229
|
+
|
|
230
|
+
// On mount, hydrate step from status so refresh returns to the correct place
|
|
231
|
+
useEffect(() => {
|
|
232
|
+
let cancelled = false;
|
|
233
|
+
const hydrate = async () => {
|
|
234
|
+
try {
|
|
235
|
+
const res = await sdk.getStatus(sdk?.getServerKey());
|
|
236
|
+
if (cancelled) return;
|
|
237
|
+
const suggested = mapStatusToStep(res);
|
|
238
|
+
if (suggested) {
|
|
239
|
+
setStep(suggested);
|
|
240
|
+
if (suggested === "STATUS") {
|
|
241
|
+
try {
|
|
242
|
+
const result = await sdk.getResult(sdk?.getServerKey());
|
|
243
|
+
setStatus(result);
|
|
244
|
+
} catch {
|
|
245
|
+
setStatus(res);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
} catch {}
|
|
250
|
+
};
|
|
251
|
+
hydrate();
|
|
252
|
+
return () => { cancelled = true; };
|
|
253
|
+
}, []);
|
|
254
|
+
|
|
255
|
+
// Capture a frame from the camera and upload as base64
|
|
256
|
+
const handleFaceCapture = async () => {
|
|
257
|
+
if (!videoRef.current) return;
|
|
258
|
+
setLoading(true);
|
|
259
|
+
try {
|
|
260
|
+
const video = videoRef.current;
|
|
261
|
+
const canvas = document.createElement("canvas");
|
|
262
|
+
const width = video.videoWidth || 640;
|
|
263
|
+
const height = video.videoHeight || 480;
|
|
264
|
+
canvas.width = width;
|
|
265
|
+
canvas.height = height;
|
|
266
|
+
const ctx = canvas.getContext("2d");
|
|
267
|
+
if (!ctx) throw new Error("Canvas not supported");
|
|
268
|
+
ctx.drawImage(video, 0, 0, width, height);
|
|
269
|
+
const dataUrl = canvas.toDataURL("image/jpeg", 0.92);
|
|
270
|
+
const base64 = dataUrl.split(",")[1] || dataUrl;
|
|
271
|
+
console.log("sdk from kycmodal",sdk,sdk?.getServerKey());
|
|
272
|
+
|
|
273
|
+
await sdk.uploadFace(await (await fetch(dataUrl)).blob(), sdk?.getServerKey());
|
|
274
|
+
setStep("DOCUMENT");
|
|
275
|
+
} catch (err: any) {
|
|
276
|
+
const msg = getDisplayError(err) || "Face capture failed";
|
|
277
|
+
// If face already exists, do not proceed further
|
|
278
|
+
if (typeof msg === "string" && msg.toLowerCase().includes("face already registered")) {
|
|
279
|
+
toast.info("Face already registered. Please proceed later or use a different session.");
|
|
280
|
+
} else {
|
|
281
|
+
toast.error(msg);
|
|
282
|
+
}
|
|
283
|
+
} finally {
|
|
284
|
+
setLoading(false);
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
// Handle document upload
|
|
289
|
+
const handleDocumentUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
290
|
+
if (loading) return; // prevent concurrent uploads
|
|
291
|
+
if (!event.target.files?.[0]) return;
|
|
292
|
+
setDocFileName(event.target.files[0].name || "");
|
|
293
|
+
setLoading(true);
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
const file = event.target.files[0];
|
|
297
|
+
// Single attempt only; user can click "Upload again" if it fails
|
|
298
|
+
await sdk.uploadDocument(file, docType, sdk?.getServerKey());
|
|
299
|
+
|
|
300
|
+
setStep("STATUS");
|
|
301
|
+
// Prefer final result if available, fallback to status
|
|
302
|
+
try {
|
|
303
|
+
const result = await sdk.getResult(sdk?.getServerKey());
|
|
304
|
+
setStatus(result);
|
|
305
|
+
} catch {
|
|
306
|
+
const res = await sdk.getStatus(sdk?.getServerKey());
|
|
307
|
+
setStatus(res);
|
|
308
|
+
}
|
|
309
|
+
} catch (err: any) {
|
|
310
|
+
const msg = getDisplayError(err);
|
|
311
|
+
if (msg && msg.toLowerCase().includes("document") && msg.toLowerCase().includes("already exists")) {
|
|
312
|
+
toast.info("Document already exists. Please do not upload again.");
|
|
313
|
+
} else {
|
|
314
|
+
toast.error(msg || "Document upload failed");
|
|
315
|
+
}
|
|
316
|
+
} finally {
|
|
317
|
+
setLoading(false);
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
return (
|
|
322
|
+
<div className={`kyc-backdrop ${mergedClassNames?.backdrop || ""}`} style={mergedStyles?.backdrop}>
|
|
323
|
+
<div className={`kyc-container ${mergedClassNames?.container || ""}`} style={mergedStyles?.container}>
|
|
324
|
+
{toastMessage && (
|
|
325
|
+
<div style={{
|
|
326
|
+
background: "#10b981",
|
|
327
|
+
color: "#ffffff",
|
|
328
|
+
padding: 8,
|
|
329
|
+
borderRadius: 8,
|
|
330
|
+
marginBottom: 8,
|
|
331
|
+
textAlign: "center"
|
|
332
|
+
}}>
|
|
333
|
+
{toastMessage}
|
|
334
|
+
</div>
|
|
335
|
+
)}
|
|
336
|
+
<button
|
|
337
|
+
onClick={onClose}
|
|
338
|
+
className={`kyc-close ${mergedClassNames?.closeButton || ""}`}
|
|
339
|
+
style={mergedStyles?.closeButton}
|
|
340
|
+
aria-label="Close"
|
|
341
|
+
>
|
|
342
|
+
✕
|
|
343
|
+
</button>
|
|
344
|
+
|
|
345
|
+
{/* Loading indicator will be shown contextually within steps */}
|
|
346
|
+
|
|
347
|
+
{step === "FACE" && (
|
|
348
|
+
<div>
|
|
349
|
+
<h2 className={`kyc-title ${mergedClassNames?.title || ""}`} style={mergedStyles?.title}>Capture Face</h2>
|
|
350
|
+
<div style={{ display: "grid", gap: 8 }}>
|
|
351
|
+
<video ref={videoRef} playsInline muted style={{ width: "100%", borderRadius: 8, background: "#000" }} />
|
|
352
|
+
<button
|
|
353
|
+
type="button"
|
|
354
|
+
disabled={!cameraReady || loading}
|
|
355
|
+
onClick={handleFaceCapture}
|
|
356
|
+
className={`kyc-input ${mergedClassNames?.input || ""}`}
|
|
357
|
+
style={{
|
|
358
|
+
...(mergedStyles?.input || {}),
|
|
359
|
+
background: cameraReady && !loading ? "linear-gradient(90deg, #FF8A00 0%, #FF3D77 100%)" : "#9ca3af",
|
|
360
|
+
color: "#ffffff",
|
|
361
|
+
border: cameraReady && !loading ? "none" : "1px solid #d1d5db",
|
|
362
|
+
borderRadius: 8
|
|
363
|
+
}}
|
|
364
|
+
>
|
|
365
|
+
{loading ? "Capturing..." : "Capture & Continue"}
|
|
366
|
+
</button>
|
|
367
|
+
|
|
368
|
+
{showQr && (
|
|
369
|
+
<div style={{ textAlign: "center", marginTop: "16px" }}>
|
|
370
|
+
<p style={{ margin: "0 0 8px 0", fontSize: "14px", color: "#6b7280" }}>
|
|
371
|
+
Or continue on mobile for better camera quality
|
|
372
|
+
</p>
|
|
373
|
+
<div style={{
|
|
374
|
+
display: "inline-block",
|
|
375
|
+
padding: "16px",
|
|
376
|
+
background: "#f9fafb",
|
|
377
|
+
border: "1px solid #e5e7eb",
|
|
378
|
+
borderRadius: "8px",
|
|
379
|
+
marginBottom: "8px"
|
|
380
|
+
}}>
|
|
381
|
+
<div style={{
|
|
382
|
+
width: "120px",
|
|
383
|
+
height: "120px",
|
|
384
|
+
background: "#ffffff",
|
|
385
|
+
border: "1px solid #d1d5db",
|
|
386
|
+
borderRadius: "4px",
|
|
387
|
+
display: "flex",
|
|
388
|
+
alignItems: "center",
|
|
389
|
+
justifyContent: "center",
|
|
390
|
+
fontSize: "10px",
|
|
391
|
+
color: "#6b7280",
|
|
392
|
+
textAlign: "center",
|
|
393
|
+
padding: "8px",
|
|
394
|
+
wordBreak: "break-all"
|
|
395
|
+
}}>
|
|
396
|
+
<img
|
|
397
|
+
src={computedQrSrc}
|
|
398
|
+
alt="QR Code for mobile continuation"
|
|
399
|
+
style={{
|
|
400
|
+
width: "100%",
|
|
401
|
+
height: "100%",
|
|
402
|
+
objectFit: "contain"
|
|
403
|
+
}}
|
|
404
|
+
/>
|
|
405
|
+
</div>
|
|
406
|
+
</div>
|
|
407
|
+
<p style={{ margin: "0", fontSize: "12px", color: "#9ca3af" }}>
|
|
408
|
+
Scan QR code with your mobile device
|
|
409
|
+
</p>
|
|
410
|
+
{mobileUrl && (
|
|
411
|
+
<p style={{ margin: "4px 0 0 0", fontSize: "10px", color: "#9ca3af" }}>
|
|
412
|
+
Or visit: <a href={mobileUrl} style={{ color: "#2563eb" }}>{mobileUrl}</a>
|
|
413
|
+
</p>
|
|
414
|
+
)}
|
|
415
|
+
</div>
|
|
416
|
+
)}
|
|
417
|
+
</div>
|
|
418
|
+
</div>
|
|
419
|
+
)}
|
|
420
|
+
|
|
421
|
+
{step === "DOCUMENT" && (
|
|
422
|
+
<div style={{ minHeight: 380, display: "flex", flexDirection: "column" }}>
|
|
423
|
+
<h2 className={`kyc-title ${mergedClassNames?.title || ""}`} style={mergedStyles?.title}>Upload Document</h2>
|
|
424
|
+
<div style={{ display: "flex", gap: 8, marginBottom: 8 }}>
|
|
425
|
+
<label style={{ display: "flex", alignItems: "center", gap: 6, color: "#000" }}>
|
|
426
|
+
<input
|
|
427
|
+
type="radio"
|
|
428
|
+
name="doc-type"
|
|
429
|
+
value="CNIC"
|
|
430
|
+
checked={docType === "CNIC"}
|
|
431
|
+
onChange={() => setDocType("CNIC")}
|
|
432
|
+
/>
|
|
433
|
+
NIC
|
|
434
|
+
</label>
|
|
435
|
+
<label style={{ display: "flex", alignItems: "center", gap: 6, color: "#000" }}>
|
|
436
|
+
<input
|
|
437
|
+
type="radio"
|
|
438
|
+
name="doc-type"
|
|
439
|
+
value="PASSPORT"
|
|
440
|
+
checked={docType === "PASSPORT"}
|
|
441
|
+
onChange={() => setDocType("PASSPORT")}
|
|
442
|
+
/>
|
|
443
|
+
Passport
|
|
444
|
+
</label>
|
|
445
|
+
</div>
|
|
446
|
+
<input
|
|
447
|
+
ref={fileInputRef}
|
|
448
|
+
type="file"
|
|
449
|
+
accept="image/*,.pdf"
|
|
450
|
+
onChange={handleDocumentUpload}
|
|
451
|
+
className={`kyc-input ${mergedClassNames?.input || ""}`}
|
|
452
|
+
style={{ ...mergedStyles?.input, display: "none" }}
|
|
453
|
+
/>
|
|
454
|
+
{loading && (
|
|
455
|
+
<p className={`kyc-loading ${mergedClassNames?.loading || ""}`} style={mergedStyles?.loading}>Processing...</p>
|
|
456
|
+
)}
|
|
457
|
+
{docFileName && (
|
|
458
|
+
<div style={{
|
|
459
|
+
marginTop: 6,
|
|
460
|
+
display: "inline-flex",
|
|
461
|
+
alignItems: "center",
|
|
462
|
+
gap: 8,
|
|
463
|
+
padding: "6px 10px",
|
|
464
|
+
borderRadius: 9999,
|
|
465
|
+
background: "#eef2ff",
|
|
466
|
+
color: "#111827",
|
|
467
|
+
fontSize: 13,
|
|
468
|
+
}}>
|
|
469
|
+
<span aria-hidden>✔</span>
|
|
470
|
+
<span>File selected</span>
|
|
471
|
+
</div>
|
|
472
|
+
)}
|
|
473
|
+
<div style={{ flex: 1 }} />
|
|
474
|
+
{!docFileName && (
|
|
475
|
+
<button
|
|
476
|
+
type="button"
|
|
477
|
+
onClick={() => fileInputRef.current?.click()}
|
|
478
|
+
className={`kyc-input ${mergedClassNames?.input || ""}`}
|
|
479
|
+
style={{
|
|
480
|
+
...mergedStyles?.input,
|
|
481
|
+
background: "linear-gradient(90deg, #FF8A00 0%, #FF3D77 100%)",
|
|
482
|
+
color: "#fff",
|
|
483
|
+
border: "none",
|
|
484
|
+
borderRadius: 8,
|
|
485
|
+
padding: 10,
|
|
486
|
+
marginTop: 12
|
|
487
|
+
}}
|
|
488
|
+
>
|
|
489
|
+
Choose file
|
|
490
|
+
</button>
|
|
491
|
+
)}
|
|
492
|
+
</div>
|
|
493
|
+
)}
|
|
494
|
+
|
|
495
|
+
{step === "STATUS" && (
|
|
496
|
+
<div>
|
|
497
|
+
<h2 className={`kyc-title ${mergedClassNames?.title || ""}`} style={mergedStyles?.title}>Verification Status</h2>
|
|
498
|
+
{status ? (
|
|
499
|
+
<div className={`kyc-status ${mergedClassNames?.statusBlock || ""}`} style={mergedStyles?.statusBlock}>
|
|
500
|
+
{(() => {
|
|
501
|
+
const data: any = status?.data ?? status;
|
|
502
|
+
const reference: string | undefined = data?.reference ?? data?.ref;
|
|
503
|
+
const email: string | undefined = data?.email;
|
|
504
|
+
const faceStatus: string | undefined = data?.face_recognition_status ?? data?.face_status ?? data?.face ?? data?.faceRecognitionStatus;
|
|
505
|
+
const kycStatus: string | undefined = data?.kyc_status ?? data?.status ?? status?.status;
|
|
506
|
+
const redirectUrl: string | undefined = data?.redirect_url ?? data?.redirectUrl;
|
|
507
|
+
return (
|
|
508
|
+
<div style={{ display: "grid", gap: 8, color: "#000", padding: 0 }}>
|
|
509
|
+
{reference && (
|
|
510
|
+
<div><strong>Reference:</strong> {reference}</div>
|
|
511
|
+
)}
|
|
512
|
+
{email && (
|
|
513
|
+
<div><strong>Email:</strong> {email}</div>
|
|
514
|
+
)}
|
|
515
|
+
{faceStatus && (
|
|
516
|
+
<div><strong>Face Status:</strong> {String(faceStatus)}</div>
|
|
517
|
+
)}
|
|
518
|
+
{kycStatus && (
|
|
519
|
+
<div><strong>KYC Status:</strong> {String(kycStatus)}</div>
|
|
520
|
+
)}
|
|
521
|
+
{redirectUrl && (
|
|
522
|
+
<div style={{ overflowWrap: "anywhere" }}>
|
|
523
|
+
<strong>Redirect URL:</strong> {redirectUrl}
|
|
524
|
+
</div>
|
|
525
|
+
)}
|
|
526
|
+
{!reference && !email && !faceStatus && !kycStatus && !redirectUrl && (
|
|
527
|
+
<pre style={{ margin: 0 }}>{JSON.stringify(status, null, 2)}</pre>
|
|
528
|
+
)}
|
|
529
|
+
</div>
|
|
530
|
+
);
|
|
531
|
+
})()}
|
|
532
|
+
</div>
|
|
533
|
+
) : (
|
|
534
|
+
<p>No status available.</p>
|
|
535
|
+
)}
|
|
536
|
+
<div style={{ marginTop: 12 }}>
|
|
537
|
+
<button
|
|
538
|
+
type="button"
|
|
539
|
+
onClick={onClose}
|
|
540
|
+
className={`kyc-input ${mergedClassNames?.input || ""}`}
|
|
541
|
+
style={{ ...(mergedStyles?.input || {}), background: "#111827", color: "#fff", border: "none", borderRadius: 8, padding: 10 }}
|
|
542
|
+
>
|
|
543
|
+
Close
|
|
544
|
+
</button>
|
|
545
|
+
</div>
|
|
546
|
+
</div>
|
|
547
|
+
)}
|
|
548
|
+
</div>
|
|
549
|
+
</div>
|
|
550
|
+
);
|
|
551
|
+
}
|
package/src/index.css
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
|
-
@tailwind base;
|
|
2
|
-
@tailwind components;
|
|
3
|
-
@tailwind utilities;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
.custom__scrollbar {
|
|
8
|
-
scrollbar-width: thin;
|
|
9
|
-
scrollbar-color: #474747 transparent;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
.custom__scrollbar::-webkit-scrollbar {
|
|
13
|
-
width: 6px;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
.custom__scrollbar::-webkit-scrollbar-track {
|
|
17
|
-
background: transparent;
|
|
18
|
-
}
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
.custom__scrollbar {
|
|
8
|
+
scrollbar-width: thin;
|
|
9
|
+
scrollbar-color: #474747 transparent;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.custom__scrollbar::-webkit-scrollbar {
|
|
13
|
+
width: 6px;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.custom__scrollbar::-webkit-scrollbar-track {
|
|
17
|
+
background: transparent;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
19
23
|
|
package/src/main.tsx
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { StrictMode } from 'react'
|
|
2
|
-
import { createRoot } from 'react-dom/client'
|
|
3
|
-
import './index.css'
|
|
4
|
-
import App from './App.tsx'
|
|
5
|
-
|
|
6
|
-
createRoot(document.getElementById('root')!).render(
|
|
7
|
-
<StrictMode>
|
|
8
|
-
<App />
|
|
9
|
-
</StrictMode>,
|
|
10
|
-
)
|
|
1
|
+
import { StrictMode } from 'react'
|
|
2
|
+
import { createRoot } from 'react-dom/client'
|
|
3
|
+
import './index.css'
|
|
4
|
+
import App from './App.tsx'
|
|
5
|
+
|
|
6
|
+
createRoot(document.getElementById('root')!).render(
|
|
7
|
+
<StrictMode>
|
|
8
|
+
<App />
|
|
9
|
+
</StrictMode>,
|
|
10
|
+
)
|
package/src/pages/QRCodePage.tsx
CHANGED
|
@@ -11,7 +11,7 @@ interface QRCodePageProps {
|
|
|
11
11
|
serverKey?: string;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
function QRCodePage({ onClose, onNavigate, mobileBaseUrl = 'https://
|
|
14
|
+
function QRCodePage({ onClose, onNavigate, mobileBaseUrl = 'https://kyc-sdk.astraprotocol.com', sessionId, apiBaseUrl, serverKey }: QRCodePageProps = {}) {
|
|
15
15
|
const [qrUrl, setQrUrl] = useState<string>('');
|
|
16
16
|
const [copied, setCopied] = useState<boolean>(false);
|
|
17
17
|
|
package/dist/.htaccess
DELETED