@splitin/verification-react 0.1.0-beta.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/LICENSE +21 -0
- package/NOTICE +32 -0
- package/README.md +14 -0
- package/dist/appeal.cjs +63 -0
- package/dist/appeal.cjs.map +1 -0
- package/dist/appeal.d.cts +21 -0
- package/dist/appeal.d.ts +21 -0
- package/dist/appeal.d.ts.map +1 -0
- package/dist/appeal.js +61 -0
- package/dist/appeal.js.map +1 -0
- package/dist/index.cjs +126 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +49 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +118 -0
- package/dist/index.js.map +1 -0
- package/dist/launcher.cjs +501 -0
- package/dist/launcher.cjs.map +1 -0
- package/dist/launcher.d.cts +39 -0
- package/dist/launcher.d.ts +39 -0
- package/dist/launcher.d.ts.map +1 -0
- package/dist/launcher.js +499 -0
- package/dist/launcher.js.map +1 -0
- package/dist/operations.cjs +225 -0
- package/dist/operations.cjs.map +1 -0
- package/dist/operations.d.cts +37 -0
- package/dist/operations.d.ts +37 -0
- package/dist/operations.d.ts.map +1 -0
- package/dist/operations.js +223 -0
- package/dist/operations.js.map +1 -0
- package/dist/retry.cjs +16 -0
- package/dist/retry.cjs.map +1 -0
- package/dist/retry.d.cts +9 -0
- package/dist/retry.d.ts +9 -0
- package/dist/retry.d.ts.map +1 -0
- package/dist/retry.js +14 -0
- package/dist/retry.js.map +1 -0
- package/dist/status.cjs +27 -0
- package/dist/status.cjs.map +1 -0
- package/dist/status.d.cts +8 -0
- package/dist/status.d.ts +8 -0
- package/dist/status.d.ts.map +1 -0
- package/dist/status.js +25 -0
- package/dist/status.js.map +1 -0
- package/dist/support.cjs +12 -0
- package/dist/support.cjs.map +1 -0
- package/dist/support.d.cts +8 -0
- package/dist/support.d.ts +8 -0
- package/dist/support.d.ts.map +1 -0
- package/dist/support.js +10 -0
- package/dist/support.js.map +1 -0
- package/package.json +92 -0
- package/src/styles.css +71 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ComponentType } from 'react';
|
|
3
|
+
import { VerificationStatusEnvelope } from '@splitin/verification-web';
|
|
4
|
+
import { AppealFormProps } from './appeal.cjs';
|
|
5
|
+
|
|
6
|
+
interface ProviderLauncherProps {
|
|
7
|
+
launchSecret: string;
|
|
8
|
+
publishableKey?: string;
|
|
9
|
+
environmentId?: string;
|
|
10
|
+
inquiryId?: string;
|
|
11
|
+
onCallback: () => void;
|
|
12
|
+
onError: (message: string) => void;
|
|
13
|
+
onOpened?: () => void;
|
|
14
|
+
onPause?: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface VerificationLauncherProps {
|
|
18
|
+
open: boolean;
|
|
19
|
+
session: VerificationStatusEnvelope;
|
|
20
|
+
onOpenChange: (open: boolean) => void;
|
|
21
|
+
onSessionChange: (session: VerificationStatusEnvelope) => void;
|
|
22
|
+
onStatusRefresh: () => Promise<boolean | VerificationStatusEnvelope>;
|
|
23
|
+
pause?: () => Promise<VerificationStatusEnvelope | {
|
|
24
|
+
status: VerificationStatusEnvelope['status'];
|
|
25
|
+
}>;
|
|
26
|
+
retry?: () => Promise<VerificationStatusEnvelope>;
|
|
27
|
+
cancel?: () => Promise<VerificationStatusEnvelope | {
|
|
28
|
+
status: VerificationStatusEnvelope['status'];
|
|
29
|
+
}>;
|
|
30
|
+
createContinuationUrl?: (token: string) => string;
|
|
31
|
+
supportHref?: string;
|
|
32
|
+
stripePublishableKey?: string;
|
|
33
|
+
personaEnvironmentId?: string;
|
|
34
|
+
onAppeal?: AppealFormProps['onSubmit'];
|
|
35
|
+
plugins?: Record<string, ComponentType<ProviderLauncherProps>>;
|
|
36
|
+
}
|
|
37
|
+
declare function VerificationLauncher({ open, session, onOpenChange, onSessionChange, onStatusRefresh, pause, retry, cancel, createContinuationUrl, supportHref, stripePublishableKey, personaEnvironmentId, onAppeal, plugins, }: VerificationLauncherProps): react.JSX.Element | null;
|
|
38
|
+
|
|
39
|
+
export { VerificationLauncher, type VerificationLauncherProps };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ComponentType } from 'react';
|
|
3
|
+
import { VerificationStatusEnvelope } from '@splitin/verification-web';
|
|
4
|
+
import { AppealFormProps } from './appeal.js';
|
|
5
|
+
|
|
6
|
+
interface ProviderLauncherProps {
|
|
7
|
+
launchSecret: string;
|
|
8
|
+
publishableKey?: string;
|
|
9
|
+
environmentId?: string;
|
|
10
|
+
inquiryId?: string;
|
|
11
|
+
onCallback: () => void;
|
|
12
|
+
onError: (message: string) => void;
|
|
13
|
+
onOpened?: () => void;
|
|
14
|
+
onPause?: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface VerificationLauncherProps {
|
|
18
|
+
open: boolean;
|
|
19
|
+
session: VerificationStatusEnvelope;
|
|
20
|
+
onOpenChange: (open: boolean) => void;
|
|
21
|
+
onSessionChange: (session: VerificationStatusEnvelope) => void;
|
|
22
|
+
onStatusRefresh: () => Promise<boolean | VerificationStatusEnvelope>;
|
|
23
|
+
pause?: () => Promise<VerificationStatusEnvelope | {
|
|
24
|
+
status: VerificationStatusEnvelope['status'];
|
|
25
|
+
}>;
|
|
26
|
+
retry?: () => Promise<VerificationStatusEnvelope>;
|
|
27
|
+
cancel?: () => Promise<VerificationStatusEnvelope | {
|
|
28
|
+
status: VerificationStatusEnvelope['status'];
|
|
29
|
+
}>;
|
|
30
|
+
createContinuationUrl?: (token: string) => string;
|
|
31
|
+
supportHref?: string;
|
|
32
|
+
stripePublishableKey?: string;
|
|
33
|
+
personaEnvironmentId?: string;
|
|
34
|
+
onAppeal?: AppealFormProps['onSubmit'];
|
|
35
|
+
plugins?: Record<string, ComponentType<ProviderLauncherProps>>;
|
|
36
|
+
}
|
|
37
|
+
declare function VerificationLauncher({ open, session, onOpenChange, onSessionChange, onStatusRefresh, pause, retry, cancel, createContinuationUrl, supportHref, stripePublishableKey, personaEnvironmentId, onAppeal, plugins, }: VerificationLauncherProps): react.JSX.Element | null;
|
|
38
|
+
|
|
39
|
+
export { VerificationLauncher, type VerificationLauncherProps };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"launcher.d.ts","sources":["launcher.d.ts"],"names":[],"mappings":"AAAA"}
|
package/dist/launcher.js
ADDED
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
import { lazy, useState, useCallback, useRef, useEffect, useMemo, Suspense } from 'react';
|
|
2
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
3
|
+
import { prefersReducedMotion, clientSurface, createQrDataUrl, isInstalledPwa, isBrowserOnline, subscribeOnlineStatus } from '@splitin/verification-web';
|
|
4
|
+
|
|
5
|
+
var __defProp = Object.defineProperty;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __esm = (fn, res) => function __init() {
|
|
8
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
9
|
+
};
|
|
10
|
+
var __export = (target, all) => {
|
|
11
|
+
for (var name in all)
|
|
12
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// src/plugins/stripe.tsx
|
|
16
|
+
var stripe_exports = {};
|
|
17
|
+
__export(stripe_exports, {
|
|
18
|
+
default: () => StripeIdentityLauncher
|
|
19
|
+
});
|
|
20
|
+
function StripeIdentityLauncher({
|
|
21
|
+
launchSecret,
|
|
22
|
+
publishableKey,
|
|
23
|
+
onCallback,
|
|
24
|
+
onError,
|
|
25
|
+
onOpened
|
|
26
|
+
}) {
|
|
27
|
+
const [opening, setOpening] = useState(false);
|
|
28
|
+
const open = useCallback(async () => {
|
|
29
|
+
if (opening) return;
|
|
30
|
+
const key = publishableKey ?? "";
|
|
31
|
+
if (!key.startsWith("pk_")) {
|
|
32
|
+
onError("Secure identity verification is not configured on this device. Use the hosted fallback or contact support.");
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
setOpening(true);
|
|
36
|
+
onOpened?.();
|
|
37
|
+
try {
|
|
38
|
+
const { loadStripe } = await import('@stripe/stripe-js');
|
|
39
|
+
const stripe = await loadStripe(key);
|
|
40
|
+
if (!stripe) throw new Error("stripe_unavailable");
|
|
41
|
+
const result = await stripe.verifyIdentity(launchSecret);
|
|
42
|
+
if (result.error) {
|
|
43
|
+
onError(result.error.message || "Identity verification could not continue. Check camera permissions or use the hosted fallback.");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
onCallback();
|
|
47
|
+
} catch {
|
|
48
|
+
onError("Identity verification could not open. Check your connection, camera permission, or use the hosted fallback.");
|
|
49
|
+
} finally {
|
|
50
|
+
setOpening(false);
|
|
51
|
+
}
|
|
52
|
+
}, [launchSecret, onCallback, onError, onOpened, opening, publishableKey]);
|
|
53
|
+
return /* @__PURE__ */ jsx("button", { type: "button", className: "siv-button", "data-variant": "primary", "data-testid": "stripe-identity-launcher", disabled: opening, onClick: () => void open(), children: opening ? "Opening secure verification\u2026" : "Verify identity securely" });
|
|
54
|
+
}
|
|
55
|
+
var init_stripe = __esm({
|
|
56
|
+
"src/plugins/stripe.tsx"() {
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// src/plugins/persona.tsx
|
|
61
|
+
var persona_exports = {};
|
|
62
|
+
__export(persona_exports, {
|
|
63
|
+
default: () => PersonaEmbeddedLauncher
|
|
64
|
+
});
|
|
65
|
+
function PersonaEmbeddedLauncher({
|
|
66
|
+
inquiryId,
|
|
67
|
+
environmentId,
|
|
68
|
+
launchSecret,
|
|
69
|
+
onCallback,
|
|
70
|
+
onError,
|
|
71
|
+
onOpened,
|
|
72
|
+
onPause
|
|
73
|
+
}) {
|
|
74
|
+
const mountRef = useRef(null);
|
|
75
|
+
const clientRef = useRef(null);
|
|
76
|
+
const generationRef = useRef(0);
|
|
77
|
+
const [ready, setReady] = useState(false);
|
|
78
|
+
const [opening, setOpening] = useState(false);
|
|
79
|
+
const prepare = useCallback(async () => {
|
|
80
|
+
if (clientRef.current || !mountRef.current || !inquiryId) return;
|
|
81
|
+
const generation = ++generationRef.current;
|
|
82
|
+
setOpening(true);
|
|
83
|
+
try {
|
|
84
|
+
const { Client } = await import('persona');
|
|
85
|
+
if (generation !== generationRef.current || !mountRef.current) return;
|
|
86
|
+
const client = new Client({
|
|
87
|
+
inquiryId,
|
|
88
|
+
environmentId,
|
|
89
|
+
...launchSecret ? { sessionToken: launchSecret } : {},
|
|
90
|
+
parent: mountRef.current,
|
|
91
|
+
frameWidth: "100%",
|
|
92
|
+
frameHeight: "100%",
|
|
93
|
+
onReady: () => {
|
|
94
|
+
if (generation !== generationRef.current) return;
|
|
95
|
+
onOpened?.();
|
|
96
|
+
setReady(true);
|
|
97
|
+
setOpening(false);
|
|
98
|
+
client.open();
|
|
99
|
+
},
|
|
100
|
+
onComplete: () => onCallback(),
|
|
101
|
+
onCancel: () => onPause?.(),
|
|
102
|
+
onError: () => {
|
|
103
|
+
setOpening(false);
|
|
104
|
+
onError("Secure verification could not open in this browser. Use the hosted or continuation option below.");
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
clientRef.current = client;
|
|
108
|
+
} catch {
|
|
109
|
+
setOpening(false);
|
|
110
|
+
onError("Secure verification could not open in this browser. Use the hosted or continuation option below.");
|
|
111
|
+
}
|
|
112
|
+
}, [environmentId, inquiryId, launchSecret, onCallback, onError, onOpened, onPause]);
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
void prepare();
|
|
115
|
+
return () => {
|
|
116
|
+
generationRef.current += 1;
|
|
117
|
+
clientRef.current?.destroy();
|
|
118
|
+
clientRef.current = null;
|
|
119
|
+
setReady(false);
|
|
120
|
+
};
|
|
121
|
+
}, [prepare]);
|
|
122
|
+
return /* @__PURE__ */ jsxs("section", { "aria-label": "Identity verification", "data-testid": "verification-persona-embedded", children: [
|
|
123
|
+
/* @__PURE__ */ jsx("div", { ref: mountRef, className: "siv-record", style: { minHeight: "min(560px, 62dvh)", position: "relative" }, children: opening ? /* @__PURE__ */ jsx("p", { className: "siv-copy", role: "status", children: "Preparing secure verification\u2026" }) : null }),
|
|
124
|
+
ready ? /* @__PURE__ */ jsx("button", { type: "button", className: "siv-button", onClick: () => {
|
|
125
|
+
onOpened?.();
|
|
126
|
+
clientRef.current?.open();
|
|
127
|
+
}, children: "Reopen secure verification" }) : null,
|
|
128
|
+
/* @__PURE__ */ jsx("p", { className: "siv-disclosure", children: "Verification technology provided by Persona." })
|
|
129
|
+
] });
|
|
130
|
+
}
|
|
131
|
+
var init_persona = __esm({
|
|
132
|
+
"src/plugins/persona.tsx"() {
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// src/plugins/plaid.tsx
|
|
137
|
+
var plaid_exports = {};
|
|
138
|
+
__export(plaid_exports, {
|
|
139
|
+
default: () => PlaidLegacyLauncher
|
|
140
|
+
});
|
|
141
|
+
function PlaidLegacyLauncher(props) {
|
|
142
|
+
const [usePlaidLink, setUsePlaidLink] = useState(null);
|
|
143
|
+
const onError = props.onError;
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
let cancelled = false;
|
|
146
|
+
void import('react-plaid-link').then((mod) => {
|
|
147
|
+
if (!cancelled) setUsePlaidLink(() => mod.usePlaidLink);
|
|
148
|
+
}).catch(() => {
|
|
149
|
+
onError("Verification could not open in this browser.");
|
|
150
|
+
});
|
|
151
|
+
return () => {
|
|
152
|
+
cancelled = true;
|
|
153
|
+
};
|
|
154
|
+
}, [onError]);
|
|
155
|
+
if (!usePlaidLink) {
|
|
156
|
+
return /* @__PURE__ */ jsx("button", { type: "button", className: "siv-button", "data-variant": "primary", disabled: true, children: "Preparing secure verification\u2026" });
|
|
157
|
+
}
|
|
158
|
+
return /* @__PURE__ */ jsx(PlaidReady, { usePlaidLink, ...props });
|
|
159
|
+
}
|
|
160
|
+
function PlaidReady({
|
|
161
|
+
usePlaidLink,
|
|
162
|
+
launchSecret,
|
|
163
|
+
onCallback,
|
|
164
|
+
onError,
|
|
165
|
+
onOpened,
|
|
166
|
+
onPause
|
|
167
|
+
}) {
|
|
168
|
+
const onSuccess = useCallback(() => onCallback(), [onCallback]);
|
|
169
|
+
const onExit = useCallback((error2) => {
|
|
170
|
+
if (error2) onError(error2.display_message || "Verification closed before it could finish.");
|
|
171
|
+
else onPause?.();
|
|
172
|
+
}, [onError, onPause]);
|
|
173
|
+
const { open, ready, error } = usePlaidLink({ token: launchSecret, onSuccess, onExit });
|
|
174
|
+
useEffect(() => {
|
|
175
|
+
if (error) onError("Verification could not open in this browser.");
|
|
176
|
+
}, [error, onError]);
|
|
177
|
+
return /* @__PURE__ */ jsx("button", { type: "button", className: "siv-button", "data-variant": "primary", disabled: !ready, onClick: () => {
|
|
178
|
+
onOpened?.();
|
|
179
|
+
open();
|
|
180
|
+
}, children: ready ? "Open secure verification" : "Preparing secure verification\u2026" });
|
|
181
|
+
}
|
|
182
|
+
var init_plaid = __esm({
|
|
183
|
+
"src/plugins/plaid.tsx"() {
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
function AppealForm({ attemptId, onSubmit, onSubmitted, onCancel }) {
|
|
187
|
+
const [reasonCode, setReasonCode] = useState("non_biometric_alternative");
|
|
188
|
+
const [busy, setBusy] = useState(false);
|
|
189
|
+
const [result, setResult] = useState(null);
|
|
190
|
+
const [error, setError] = useState(null);
|
|
191
|
+
return /* @__PURE__ */ jsxs(
|
|
192
|
+
"form",
|
|
193
|
+
{
|
|
194
|
+
className: "siv siv-dialog",
|
|
195
|
+
"aria-labelledby": "siv-appeal-title",
|
|
196
|
+
onSubmit: (event) => {
|
|
197
|
+
event.preventDefault();
|
|
198
|
+
if (!onSubmit) {
|
|
199
|
+
setError("Appeal submission is not configured.");
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
setBusy(true);
|
|
203
|
+
void onSubmit({
|
|
204
|
+
attemptId,
|
|
205
|
+
reasonCode,
|
|
206
|
+
nonBiometricPathRequested: reasonCode === "non_biometric_alternative"
|
|
207
|
+
}).then((next) => {
|
|
208
|
+
setResult(next);
|
|
209
|
+
onSubmitted?.(next);
|
|
210
|
+
}).catch((cause) => {
|
|
211
|
+
setError(cause instanceof Error ? cause.message : "Verification review could not be requested.");
|
|
212
|
+
}).finally(() => setBusy(false));
|
|
213
|
+
},
|
|
214
|
+
children: [
|
|
215
|
+
/* @__PURE__ */ jsx("h2", { className: "siv-title", id: "siv-appeal-title", children: "Request a verification review" }),
|
|
216
|
+
/* @__PURE__ */ jsx("p", { className: "siv-copy", children: "Support sees only a safe review reference. Identity documents remain with the verification provider." }),
|
|
217
|
+
/* @__PURE__ */ jsxs("label", { className: "siv-field", children: [
|
|
218
|
+
/* @__PURE__ */ jsx("span", { children: "What do you need?" }),
|
|
219
|
+
/* @__PURE__ */ jsxs("select", { className: "siv-select", value: reasonCode, onChange: (event) => setReasonCode(event.target.value), children: [
|
|
220
|
+
/* @__PURE__ */ jsx("option", { value: "non_biometric_alternative", children: "Cannot use a selfie" }),
|
|
221
|
+
/* @__PURE__ */ jsx("option", { value: "decision_appeal", children: "Appeal a decision" }),
|
|
222
|
+
/* @__PURE__ */ jsx("option", { value: "accessibility_support", children: "Accessibility support" })
|
|
223
|
+
] })
|
|
224
|
+
] }),
|
|
225
|
+
result ? /* @__PURE__ */ jsxs("p", { role: "status", children: [
|
|
226
|
+
"Your review is open. Reference ",
|
|
227
|
+
result.appealId.slice(0, 8),
|
|
228
|
+
"."
|
|
229
|
+
] }) : null,
|
|
230
|
+
error ? /* @__PURE__ */ jsx("p", { role: "alert", children: error }) : null,
|
|
231
|
+
/* @__PURE__ */ jsxs("div", { className: "siv-actions", children: [
|
|
232
|
+
/* @__PURE__ */ jsx("button", { type: "button", className: "siv-button", onClick: onCancel, children: "Close" }),
|
|
233
|
+
/* @__PURE__ */ jsx("button", { type: "submit", className: "siv-button", "data-variant": "primary", disabled: busy || !onSubmit, children: "Submit review request" })
|
|
234
|
+
] })
|
|
235
|
+
]
|
|
236
|
+
}
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// src/copy.ts
|
|
241
|
+
function verificationCopy(packageCode) {
|
|
242
|
+
switch (packageCode) {
|
|
243
|
+
case "business_kyb":
|
|
244
|
+
return {
|
|
245
|
+
title: "Secure business verification",
|
|
246
|
+
description: "The legal business is verified separately from your identity. You can pause and resume without starting over.",
|
|
247
|
+
reviewTitle: "Business review is in progress"
|
|
248
|
+
};
|
|
249
|
+
case "ownership_review":
|
|
250
|
+
return {
|
|
251
|
+
title: "Ownership and authority review",
|
|
252
|
+
description: "We are reviewing authority for this organization. Your action stays saved.",
|
|
253
|
+
reviewTitle: "Ownership review is in progress"
|
|
254
|
+
};
|
|
255
|
+
case "associated_person_idv":
|
|
256
|
+
return {
|
|
257
|
+
title: "Associated person verification",
|
|
258
|
+
description: "This identity check supports a separate business relationship review.",
|
|
259
|
+
reviewTitle: "Relationship review is in progress"
|
|
260
|
+
};
|
|
261
|
+
default:
|
|
262
|
+
return {
|
|
263
|
+
title: "Secure identity verification",
|
|
264
|
+
description: "You can pause and resume without starting over. Your action stays saved.",
|
|
265
|
+
reviewTitle: "Identity review is in progress"
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
var CAMERA_HELP = [
|
|
270
|
+
"Allow camera access in browser settings, close other camera apps, then retry.",
|
|
271
|
+
"Use an up-to-date Safari, Chrome, Firefox, or Edge browser. If an in-app browser blocks the camera, open the continuation link in your main browser."
|
|
272
|
+
];
|
|
273
|
+
function useOnlineStatus() {
|
|
274
|
+
const [online, setOnline] = useState(isBrowserOnline);
|
|
275
|
+
useEffect(() => subscribeOnlineStatus(setOnline), []);
|
|
276
|
+
return online;
|
|
277
|
+
}
|
|
278
|
+
function useFocusRecovery(active) {
|
|
279
|
+
useEffect(() => {
|
|
280
|
+
if (!active || typeof document === "undefined") return;
|
|
281
|
+
const previous = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
282
|
+
return () => {
|
|
283
|
+
previous?.focus();
|
|
284
|
+
};
|
|
285
|
+
}, [active]);
|
|
286
|
+
}
|
|
287
|
+
function useBusy() {
|
|
288
|
+
const [busy, setBusy] = useState(null);
|
|
289
|
+
const run = useCallback(async (key, task) => {
|
|
290
|
+
if (busy) return;
|
|
291
|
+
setBusy(key);
|
|
292
|
+
try {
|
|
293
|
+
await task();
|
|
294
|
+
} finally {
|
|
295
|
+
setBusy(null);
|
|
296
|
+
}
|
|
297
|
+
}, [busy]);
|
|
298
|
+
return [busy, run];
|
|
299
|
+
}
|
|
300
|
+
function SupportLink({ href = "/support/verification", children = "Contact support" }) {
|
|
301
|
+
return /* @__PURE__ */ jsx("a", { className: "siv-link", href, rel: "noopener noreferrer", children });
|
|
302
|
+
}
|
|
303
|
+
var StripeIdentityLauncher2 = lazy(() => Promise.resolve().then(() => (init_stripe(), stripe_exports)));
|
|
304
|
+
var PersonaEmbeddedLauncher2 = lazy(() => Promise.resolve().then(() => (init_persona(), persona_exports)));
|
|
305
|
+
var PlaidLegacyLauncher2 = lazy(() => Promise.resolve().then(() => (init_plaid(), plaid_exports)));
|
|
306
|
+
function VerificationLauncher({
|
|
307
|
+
open,
|
|
308
|
+
session,
|
|
309
|
+
onOpenChange,
|
|
310
|
+
onSessionChange,
|
|
311
|
+
onStatusRefresh,
|
|
312
|
+
pause,
|
|
313
|
+
retry,
|
|
314
|
+
cancel,
|
|
315
|
+
createContinuationUrl,
|
|
316
|
+
supportHref = "/support/verification",
|
|
317
|
+
stripePublishableKey,
|
|
318
|
+
personaEnvironmentId,
|
|
319
|
+
onAppeal,
|
|
320
|
+
plugins
|
|
321
|
+
}) {
|
|
322
|
+
const online = useOnlineStatus();
|
|
323
|
+
const [busy, run] = useBusy();
|
|
324
|
+
const [message, setMessage] = useState(null);
|
|
325
|
+
const [qrDataUrl, setQrDataUrl] = useState(null);
|
|
326
|
+
const [appealOpen, setAppealOpen] = useState(false);
|
|
327
|
+
const reducedMotion = prefersReducedMotion();
|
|
328
|
+
const surface = clientSurface();
|
|
329
|
+
const mediaDevicesMissing = typeof navigator === "undefined" || !navigator.mediaDevices;
|
|
330
|
+
const [started, setStarted] = useState(!reducedMotion);
|
|
331
|
+
const [cameraHelp, setCameraHelp] = useState(mediaDevicesMissing);
|
|
332
|
+
const dialogRef = useRef(null);
|
|
333
|
+
useFocusRecovery(open);
|
|
334
|
+
const continuationUrl = useMemo(() => {
|
|
335
|
+
if (!session.continuation) return null;
|
|
336
|
+
if (createContinuationUrl) return createContinuationUrl(session.continuation.token);
|
|
337
|
+
return session.continuation.token;
|
|
338
|
+
}, [createContinuationUrl, session.continuation]);
|
|
339
|
+
useEffect(() => {
|
|
340
|
+
let live = true;
|
|
341
|
+
setQrDataUrl(null);
|
|
342
|
+
if (!continuationUrl) return;
|
|
343
|
+
void createQrDataUrl(continuationUrl).then((url) => {
|
|
344
|
+
if (live) setQrDataUrl(url);
|
|
345
|
+
});
|
|
346
|
+
return () => {
|
|
347
|
+
live = false;
|
|
348
|
+
};
|
|
349
|
+
}, [continuationUrl]);
|
|
350
|
+
const refresh = useCallback(async () => {
|
|
351
|
+
await run("refresh", async () => {
|
|
352
|
+
const result = await onStatusRefresh();
|
|
353
|
+
if (typeof result === "object") {
|
|
354
|
+
onSessionChange(result);
|
|
355
|
+
setMessage(result.status === "verified" ? "Verified. Returning to your saved action\u2026" : "Your status is not approved yet. We\u2019ll keep your action saved.");
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
setMessage(result ? "Verified. Returning to your saved action\u2026" : "Your status is not approved yet. We\u2019ll keep your action saved.");
|
|
359
|
+
});
|
|
360
|
+
}, [onSessionChange, onStatusRefresh, run]);
|
|
361
|
+
const providerCallback = useCallback(() => {
|
|
362
|
+
setMessage("Verification was submitted. Checking the server-side result now.");
|
|
363
|
+
void refresh();
|
|
364
|
+
}, [refresh]);
|
|
365
|
+
const handlePluginError = useCallback((errorMessage) => {
|
|
366
|
+
setMessage(errorMessage);
|
|
367
|
+
if (/camera|permission/i.test(errorMessage) || typeof navigator === "undefined" || !navigator.mediaDevices) {
|
|
368
|
+
setCameraHelp(true);
|
|
369
|
+
}
|
|
370
|
+
}, []);
|
|
371
|
+
const pauseAndClose = useCallback(async () => {
|
|
372
|
+
await run("pause", async () => {
|
|
373
|
+
if (pause) {
|
|
374
|
+
const result = await pause();
|
|
375
|
+
if ("attemptId" in result) onSessionChange(result);
|
|
376
|
+
else onSessionChange({ ...session, status: result.status, canResume: true });
|
|
377
|
+
}
|
|
378
|
+
onOpenChange(false);
|
|
379
|
+
});
|
|
380
|
+
}, [onOpenChange, onSessionChange, pause, run, session]);
|
|
381
|
+
if (!open) return null;
|
|
382
|
+
const launcherKey = session.launch?.launcherKey ?? session.launcherKey ?? "none";
|
|
383
|
+
const blocked = session.status === "processing" || session.status === "manual_review_required";
|
|
384
|
+
const unavailable = session.status === "provider_unavailable" || !online;
|
|
385
|
+
const nonInteractive = session.presentation === "none";
|
|
386
|
+
const copy = verificationCopy(session.packageCode);
|
|
387
|
+
const launchSecret = session.launch?.transientSecret ?? "";
|
|
388
|
+
const CustomPlugin = plugins?.[launcherKey];
|
|
389
|
+
const pluginProps = {
|
|
390
|
+
launchSecret,
|
|
391
|
+
publishableKey: stripePublishableKey,
|
|
392
|
+
inquiryId: session.launch?.continuationReference,
|
|
393
|
+
environmentId: personaEnvironmentId,
|
|
394
|
+
onCallback: providerCallback,
|
|
395
|
+
onError: handlePluginError,
|
|
396
|
+
onOpened: () => void 0,
|
|
397
|
+
onPause: () => {
|
|
398
|
+
void pauseAndClose();
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
402
|
+
/* @__PURE__ */ jsxs(
|
|
403
|
+
"div",
|
|
404
|
+
{
|
|
405
|
+
className: "siv siv-dialog",
|
|
406
|
+
role: "dialog",
|
|
407
|
+
"aria-modal": "true",
|
|
408
|
+
"aria-labelledby": "siv-launcher-title",
|
|
409
|
+
"data-testid": "verification-launcher",
|
|
410
|
+
"data-reduced-motion": reducedMotion ? "true" : "false",
|
|
411
|
+
"data-surface": surface,
|
|
412
|
+
ref: dialogRef,
|
|
413
|
+
children: [
|
|
414
|
+
/* @__PURE__ */ jsxs("header", { className: "siv-header", children: [
|
|
415
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
416
|
+
/* @__PURE__ */ jsx("h2", { className: "siv-title", id: "siv-launcher-title", children: copy.title }),
|
|
417
|
+
/* @__PURE__ */ jsx("p", { className: "siv-copy", children: copy.description })
|
|
418
|
+
] }),
|
|
419
|
+
/* @__PURE__ */ jsx("button", { type: "button", className: "siv-button", "aria-label": "Pause verification and close", onClick: () => void pauseAndClose(), disabled: Boolean(busy), children: "Pause" })
|
|
420
|
+
] }),
|
|
421
|
+
/* @__PURE__ */ jsxs("div", { "aria-live": "polite", "aria-atomic": "true", className: "siv-live", children: [
|
|
422
|
+
!online ? /* @__PURE__ */ jsx("p", { className: "siv-alert", role: "status", children: "You are offline. Your place is saved. Reconnect to resume verification." }) : null,
|
|
423
|
+
blocked ? /* @__PURE__ */ jsxs("p", { className: "siv-alert", role: "status", children: [
|
|
424
|
+
session.status === "manual_review_required" ? "A review is in progress." : "We are checking your verification.",
|
|
425
|
+
" The protected action stays blocked until an approved server-side decision arrives."
|
|
426
|
+
] }) : null,
|
|
427
|
+
nonInteractive && !blocked && !unavailable ? /* @__PURE__ */ jsxs("p", { className: "siv-alert", role: "status", children: [
|
|
428
|
+
copy.reviewTitle,
|
|
429
|
+
". No additional browser step is required right now."
|
|
430
|
+
] }) : null,
|
|
431
|
+
unavailable && online ? /* @__PURE__ */ jsx("p", { className: "siv-alert", role: "status", children: "Verification is temporarily unavailable. Retry later, continue on another device, or contact support." }) : null,
|
|
432
|
+
message ? /* @__PURE__ */ jsx("p", { className: "siv-alert", role: "status", children: message }) : null,
|
|
433
|
+
cameraHelp ? /* @__PURE__ */ jsxs("div", { "data-testid": "verification-camera-help", className: "siv-alert", role: "status", children: [
|
|
434
|
+
CAMERA_HELP.map((line) => /* @__PURE__ */ jsx("p", { className: "siv-copy", children: line }, line)),
|
|
435
|
+
/* @__PURE__ */ jsxs("p", { className: "siv-copy", children: [
|
|
436
|
+
/* @__PURE__ */ jsx("strong", { children: "Continue on another device" }),
|
|
437
|
+
" if this camera cannot open."
|
|
438
|
+
] })
|
|
439
|
+
] }) : null
|
|
440
|
+
] }),
|
|
441
|
+
reducedMotion && !started && !blocked && !unavailable ? /* @__PURE__ */ jsx("button", { type: "button", className: "siv-button", "data-variant": "primary", onClick: () => setStarted(true), children: "Start verification" }) : null,
|
|
442
|
+
started && !blocked && !unavailable && CustomPlugin ? /* @__PURE__ */ jsx(CustomPlugin, { ...pluginProps }) : null,
|
|
443
|
+
started && !blocked && !unavailable && !CustomPlugin && launcherKey === "persona_embedded" && session.launch ? /* @__PURE__ */ jsx(Suspense, { fallback: /* @__PURE__ */ jsx("p", { role: "status", children: "Preparing secure verification\u2026" }), children: /* @__PURE__ */ jsx(PersonaEmbeddedLauncher2, { ...pluginProps, inquiryId: session.launch.continuationReference }) }) : null,
|
|
444
|
+
started && !blocked && !unavailable && !CustomPlugin && launcherKey === "plaid_link" && launchSecret ? /* @__PURE__ */ jsx(Suspense, { fallback: /* @__PURE__ */ jsx("p", { role: "status", children: "Preparing secure verification\u2026" }), children: /* @__PURE__ */ jsx(PlaidLegacyLauncher2, { ...pluginProps }) }) : null,
|
|
445
|
+
started && !blocked && !unavailable && !CustomPlugin && launcherKey === "stripe_identity" && launchSecret ? /* @__PURE__ */ jsx(Suspense, { fallback: /* @__PURE__ */ jsx("p", { role: "status", children: "Preparing secure verification\u2026" }), children: /* @__PURE__ */ jsx(StripeIdentityLauncher2, { ...pluginProps }) }) : null,
|
|
446
|
+
session.providerDisclosure ? /* @__PURE__ */ jsx("p", { className: "siv-disclosure", "data-testid": "verification-provider-disclosure", children: session.providerDisclosure }) : null,
|
|
447
|
+
session.launch?.hostedUrl ? /* @__PURE__ */ jsx("a", { className: "siv-link", "data-testid": "verification-hosted-fallback", href: session.launch.hostedUrl, target: "_blank", rel: "noopener noreferrer", children: "Open hosted verification" }) : null,
|
|
448
|
+
!nonInteractive ? /* @__PURE__ */ jsxs("section", { className: "siv-grid", "aria-label": "Continue on another device", "data-emphasis": cameraHelp ? "true" : "false", children: [
|
|
449
|
+
/* @__PURE__ */ jsx("p", { className: "siv-copy", children: "Use a one-time link or scan the QR code. The link contains no provider result and expires shortly." }),
|
|
450
|
+
/* @__PURE__ */ jsx("button", { type: "button", className: "siv-button", disabled: !continuationUrl, onClick: () => continuationUrl && void navigator.clipboard.writeText(continuationUrl).then(() => setMessage("Secure continuation link copied. It expires shortly and works once.")), children: "Copy secure continuation link" }),
|
|
451
|
+
/* @__PURE__ */ jsx("div", { "data-testid": "verification-qr-fallback", children: qrDataUrl ? /* @__PURE__ */ jsx(
|
|
452
|
+
"img",
|
|
453
|
+
{
|
|
454
|
+
src: qrDataUrl,
|
|
455
|
+
alt: "QR code to continue verification on another device",
|
|
456
|
+
width: 144,
|
|
457
|
+
height: 144,
|
|
458
|
+
className: reducedMotion ? void 0 : "siv-qr-animate"
|
|
459
|
+
}
|
|
460
|
+
) : /* @__PURE__ */ jsx("p", { className: "siv-copy", children: "QR code unavailable. Use the continuation link." }) })
|
|
461
|
+
] }) : null,
|
|
462
|
+
!nonInteractive ? /* @__PURE__ */ jsxs("details", { "data-testid": "verification-browser-troubleshooting", children: [
|
|
463
|
+
/* @__PURE__ */ jsx("summary", { children: "Camera or browser trouble?" }),
|
|
464
|
+
CAMERA_HELP.map((line) => /* @__PURE__ */ jsx("p", { className: "siv-copy", children: line }, line)),
|
|
465
|
+
isInstalledPwa() ? /* @__PURE__ */ jsx("p", { className: "siv-copy", children: "This app is installed. If the camera is blocked, open the continuation link in your system browser." }) : null,
|
|
466
|
+
/* @__PURE__ */ jsx(SupportLink, { href: supportHref })
|
|
467
|
+
] }) : null,
|
|
468
|
+
/* @__PURE__ */ jsxs("footer", { className: "siv-footer siv-actions", children: [
|
|
469
|
+
/* @__PURE__ */ jsx("button", { type: "button", className: "siv-button", "data-variant": "danger", onClick: () => void run("cancel", async () => {
|
|
470
|
+
if (!cancel) return;
|
|
471
|
+
const result = await cancel();
|
|
472
|
+
onSessionChange("attemptId" in result ? result : { ...session, status: result.status, canResume: false, canRetry: true });
|
|
473
|
+
setMessage("Verification was canceled. Your saved action was not submitted.");
|
|
474
|
+
}), disabled: Boolean(busy), children: "Cancel verification" }),
|
|
475
|
+
/* @__PURE__ */ jsx("button", { type: "button", className: "siv-button", onClick: () => setAppealOpen(true), disabled: Boolean(busy), children: "Request a review" }),
|
|
476
|
+
/* @__PURE__ */ jsx("button", { type: "button", className: "siv-button", onClick: () => void run("retry", async () => {
|
|
477
|
+
if (!retry || !session.canRetry || !online) return;
|
|
478
|
+
onSessionChange(await retry());
|
|
479
|
+
}), disabled: Boolean(busy) || !session.canRetry || !online, children: "Retry" }),
|
|
480
|
+
/* @__PURE__ */ jsx("button", { type: "button", className: "siv-button", "data-variant": "primary", onClick: () => void refresh(), disabled: Boolean(busy) || !online, children: "Check status" })
|
|
481
|
+
] })
|
|
482
|
+
]
|
|
483
|
+
}
|
|
484
|
+
),
|
|
485
|
+
appealOpen ? /* @__PURE__ */ jsx(
|
|
486
|
+
AppealForm,
|
|
487
|
+
{
|
|
488
|
+
attemptId: session.attemptId,
|
|
489
|
+
onSubmit: onAppeal,
|
|
490
|
+
onSubmitted: () => setAppealOpen(false),
|
|
491
|
+
onCancel: () => setAppealOpen(false)
|
|
492
|
+
}
|
|
493
|
+
) : null
|
|
494
|
+
] });
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
export { VerificationLauncher };
|
|
498
|
+
//# sourceMappingURL=launcher.js.map
|
|
499
|
+
//# sourceMappingURL=launcher.js.map
|