@snapkyc-ooru/consent-handoff-react 0.1.0 → 0.1.1
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 +257 -103
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,158 +1,312 @@
|
|
|
1
|
-
# @snapkyc/consent-handoff-react
|
|
1
|
+
# @snapkyc-ooru/consent-handoff-react
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> **KYC consent & identity handoff flow for React** — agreement → consent → QR / deeplink → polling → verified.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@snapkyc-ooru/consent-handoff-react)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[](https://reactjs.org)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## What is this?
|
|
12
|
+
|
|
13
|
+
`@snapkyc-ooru/consent-handoff-react` is a plug-and-play React + TypeScript SDK that orchestrates the full **SnapKYC identity-verification handoff** inside your product — without you needing to manage a single API call or UI state machine.
|
|
14
|
+
|
|
15
|
+
Drop in a single component (or call one function) and your users move through:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
Integration agreement → Purpose consent → QR code / app deeplink
|
|
19
|
+
→ Live status polling → Verification summary
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Your backend mints a short-lived **`integrationToken`**; the SDK handles everything else.
|
|
23
|
+
|
|
24
|
+
---
|
|
4
25
|
|
|
5
26
|
## Install
|
|
6
27
|
|
|
7
28
|
```bash
|
|
8
|
-
npm install @snapkyc/consent-handoff-react react react-dom
|
|
29
|
+
npm install @snapkyc-ooru/consent-handoff-react react react-dom
|
|
9
30
|
```
|
|
10
31
|
|
|
32
|
+
**Peer dependencies:** React `>= 17`, react-dom `>= 17`
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
11
36
|
## Styles
|
|
12
37
|
|
|
38
|
+
Import the bundled stylesheet once — typically in your app root or entry file:
|
|
39
|
+
|
|
13
40
|
```tsx
|
|
14
|
-
import "@snapkyc/consent-handoff-react/styles.css";
|
|
41
|
+
import "@snapkyc-ooru/consent-handoff-react/styles.css";
|
|
15
42
|
```
|
|
16
43
|
|
|
17
|
-
|
|
44
|
+
> All design tokens are scoped to `.snap-consent-handoff`. Override them via the `theme` prop rather than targeting `:root` in your host app.
|
|
18
45
|
|
|
19
|
-
|
|
46
|
+
---
|
|
20
47
|
|
|
21
|
-
|
|
48
|
+
## The 5-step flow
|
|
22
49
|
|
|
23
|
-
|
|
|
24
|
-
|
|
25
|
-
|
|
|
26
|
-
|
|
|
27
|
-
|
|
|
28
|
-
|
|
|
29
|
-
|
|
|
30
|
-
| `text` / `textMuted` | Body and secondary text |
|
|
31
|
-
| `border` / `borderActive` | Default and emphasized borders |
|
|
32
|
-
| `modalOverlay` | Scrim behind the modal |
|
|
33
|
-
| `danger` | Error text |
|
|
34
|
-
| `success` | Success verification icon and status checkmarks |
|
|
35
|
-
| `successStatusSurface` | Success screen status summary card background |
|
|
36
|
-
| `successPurposesSurface` | Success screen “Confirmed purposes” panel background |
|
|
37
|
-
| `successAccordionBorder` | Demographic accordion borders |
|
|
38
|
-
| `radius` | Corner radius string (e.g. `"12px"`) |
|
|
39
|
-
| `fontFamily` | UI font stack |
|
|
40
|
-
| `gap`, `maxWidth`, `modalZ` | Layout tokens |
|
|
50
|
+
| Step | Screen |
|
|
51
|
+
|------|--------|
|
|
52
|
+
| 1 | **Agreement** — integration notice card; optional custom `title` |
|
|
53
|
+
| 2 | **Consent** — purpose checkboxes with **Cancel** / **I Agree** |
|
|
54
|
+
| 3 | **Handoff** — QR scan *or* app deeplink (auto-detects iOS/Android) |
|
|
55
|
+
| 4 | **Polling** — live status until terminal result |
|
|
56
|
+
| 5 | **Success** — status strip, confirmed purposes, demographic accordions; **Done** |
|
|
41
57
|
|
|
42
|
-
|
|
43
|
-
import {
|
|
44
|
-
SnapConsentHandoffFlow,
|
|
45
|
-
DEFAULT_SNAP_CONSENT_THEME,
|
|
46
|
-
type SnapConsentHandoffTheme,
|
|
47
|
-
} from "@snapkyc/consent-handoff-react";
|
|
58
|
+
Dismiss controls (**×**, **Cancel**, **Done**) all call `onRequestClose` and cleanly abort any in-flight work.
|
|
48
59
|
|
|
49
|
-
|
|
50
|
-
primary: "#0f766e",
|
|
51
|
-
borderActive: "#0f766e",
|
|
52
|
-
surfaceMuted: "#f0fdfa",
|
|
53
|
-
};
|
|
60
|
+
---
|
|
54
61
|
|
|
55
|
-
|
|
56
|
-
```
|
|
62
|
+
## Quick start
|
|
57
63
|
|
|
58
|
-
|
|
64
|
+
### Option A — Imperative modal (recommended)
|
|
59
65
|
|
|
60
|
-
|
|
66
|
+
Mounts outside your React tree on `document.body`. Ideal for portals, dashboards, or any flow that should not disrupt your existing layout.
|
|
61
67
|
|
|
62
68
|
```tsx
|
|
63
|
-
import {
|
|
64
|
-
import
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
69
|
+
import { useCallback, useRef } from "react";
|
|
70
|
+
import {
|
|
71
|
+
openSnapConsentHandoffFlow,
|
|
72
|
+
type CompletionResult,
|
|
73
|
+
type FlowEvent,
|
|
74
|
+
} from "@snapkyc-ooru/consent-handoff-react";
|
|
75
|
+
import "@snapkyc-ooru/consent-handoff-react/styles.css";
|
|
76
|
+
|
|
77
|
+
function VerifyButton() {
|
|
78
|
+
const disposeRef = useRef<(() => void) | null>(null);
|
|
79
|
+
|
|
80
|
+
const startVerification = useCallback(async () => {
|
|
81
|
+
const integrationToken = await fetchTokenFromYourBackend(); // short-lived token
|
|
82
|
+
|
|
83
|
+
disposeRef.current?.();
|
|
84
|
+
disposeRef.current = openSnapConsentHandoffFlow({
|
|
85
|
+
integrationToken,
|
|
86
|
+
rpContext: {
|
|
87
|
+
reference_id: "user-or-order-id",
|
|
88
|
+
session_id: "optional-correlation-id",
|
|
89
|
+
},
|
|
90
|
+
handoffMode: "qr", // or "intent"
|
|
91
|
+
title: "Loan Application KYC Verification",
|
|
92
|
+
pollIntervalMs: 1500,
|
|
93
|
+
pollDeadlineMs: 120_000,
|
|
94
|
+
theme: {
|
|
95
|
+
primary: "#0f766e",
|
|
96
|
+
borderActive: "#0f766e",
|
|
97
|
+
surfaceMuted: "#f0fdfa",
|
|
98
|
+
},
|
|
99
|
+
onFlowEvent(evt: FlowEvent) {
|
|
100
|
+
console.log("phase →", evt);
|
|
101
|
+
// agreement-loading | agreement-ready | txn-created
|
|
102
|
+
// polling | completed | failed | cancelled
|
|
103
|
+
},
|
|
104
|
+
onSuccess(result: CompletionResult) {
|
|
105
|
+
// result.txn_id — transaction identifier
|
|
106
|
+
// result.status — terminal status string
|
|
107
|
+
// result.raw — full API payload (demographics, purposes, evidence)
|
|
108
|
+
},
|
|
109
|
+
onError(err) {
|
|
110
|
+
console.error(err);
|
|
111
|
+
},
|
|
112
|
+
}).dispose;
|
|
113
|
+
}, []);
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<button type="button" onClick={() => void startVerification()}>
|
|
117
|
+
Verify identity
|
|
118
|
+
</button>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
81
121
|
```
|
|
82
122
|
|
|
83
|
-
`
|
|
123
|
+
**Important:** store the returned `dispose` reference and call it on component unmount or navigation. The modal's built-in dismiss buttons do this automatically when using `openSnapConsentHandoffFlow`.
|
|
84
124
|
|
|
85
|
-
|
|
125
|
+
> `openSnapConsentQrFlow` is a **deprecated alias** — use `openSnapConsentHandoffFlow` going forward.
|
|
86
126
|
|
|
87
|
-
|
|
127
|
+
---
|
|
88
128
|
|
|
89
|
-
|
|
129
|
+
### Option B — Declarative component
|
|
90
130
|
|
|
91
|
-
|
|
131
|
+
Embed the flow inline inside a wizard step, settings page, or your own modal shell.
|
|
132
|
+
|
|
133
|
+
#### Inline (embedded)
|
|
92
134
|
|
|
93
135
|
```tsx
|
|
94
|
-
import {
|
|
95
|
-
import
|
|
136
|
+
import { useState } from "react";
|
|
137
|
+
import {
|
|
138
|
+
SnapConsentHandoffFlow,
|
|
139
|
+
type CompletionResult,
|
|
140
|
+
} from "@snapkyc-ooru/consent-handoff-react";
|
|
141
|
+
import "@snapkyc-ooru/consent-handoff-react/styles.css";
|
|
142
|
+
|
|
143
|
+
export function KycStep({ token }: { token: string }) {
|
|
144
|
+
const [hidden, setHidden] = useState(false);
|
|
145
|
+
if (hidden) return null;
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<SnapConsentHandoffFlow
|
|
149
|
+
integrationToken={token}
|
|
150
|
+
rpContext={{ reference_id: "USER-123", session_id: "sess-abc" }}
|
|
151
|
+
handoffMode="intent"
|
|
152
|
+
displayMode="inline"
|
|
153
|
+
title="Loan Application KYC Verification"
|
|
154
|
+
acceptLanguage="en"
|
|
155
|
+
theme={{ primary: "#0f766e" }}
|
|
156
|
+
onSuccess={(result: CompletionResult) => {
|
|
157
|
+
// result.raw contains demographics, purposes, evidence
|
|
158
|
+
}}
|
|
159
|
+
onRequestClose={() => setHidden(true)}
|
|
160
|
+
/>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
```
|
|
96
164
|
|
|
165
|
+
#### Inside your own modal shell
|
|
166
|
+
|
|
167
|
+
```tsx
|
|
97
168
|
<SnapConsentHandoffFlow
|
|
98
169
|
integrationToken={token}
|
|
99
|
-
rpContext={{ reference_id
|
|
100
|
-
handoffMode="
|
|
101
|
-
displayMode="
|
|
102
|
-
|
|
170
|
+
rpContext={{ reference_id: "USER-123" }}
|
|
171
|
+
handoffMode="qr"
|
|
172
|
+
displayMode="modal"
|
|
173
|
+
onRequestClose={() => setShowModal(false)}
|
|
174
|
+
/>
|
|
103
175
|
```
|
|
104
176
|
|
|
105
|
-
|
|
177
|
+
| `displayMode` | Behaviour |
|
|
178
|
+
|---------------|-----------|
|
|
179
|
+
| `"modal"` (default) | SDK renders its own scrim + header **×** |
|
|
180
|
+
| `"inline"` | Borderless sheet; no scrim — sits inside your layout |
|
|
106
181
|
|
|
107
|
-
|
|
108
|
-
|------|---------|
|
|
109
|
-
| `integrationToken` | Sent as `Authorization: Token …` on every request |
|
|
110
|
-
| `rpContext.reference_id` | Sent as `rp_context.ref_id` on `POST …/transactions/initiate` |
|
|
111
|
-
| `handoffMode` | `"qr"` → `POST …/generate-qr` (PNG). `"intent"` → `POST …/generate-intent?platform=` |
|
|
112
|
-
| `agreementId?` | Reserved; agreement is implied by the integration token |
|
|
113
|
-
| `acceptLanguage?` | `Accept-Language` + preference for `agreement_text` locale |
|
|
114
|
-
| `qrRequestBody?` | QR overrides (`size`; `txn_id` is set by the SDK) |
|
|
115
|
-
| `pollIntervalMs` / `pollDeadlineMs` | Polling behaviour after handoff |
|
|
116
|
-
| `theme?` | Partial palette / layout overrides (see **Theme**) |
|
|
117
|
-
| `onRequestClose?` | Called when the user closes the modal (**×**), taps **Cancel** on the consent step, or taps **Done** after success. With `openSnapConsentHandoffFlow`, this is wired to `dispose()`. If omitted (e.g. inline embed), the SDK hides its own UI after those actions. |
|
|
182
|
+
---
|
|
118
183
|
|
|
119
|
-
##
|
|
184
|
+
## Handoff modes
|
|
120
185
|
|
|
121
|
-
`
|
|
186
|
+
| `handoffMode` | What happens after I Agree |
|
|
187
|
+
|---------------|---------------------------|
|
|
188
|
+
| `"qr"` | Generates a **PNG QR code** (`POST …/generate-qr`). User scans with the authorised app. |
|
|
189
|
+
| `"intent"` | Generates an **app deeplink** (`POST …/generate-intent?platform=`). User opens native app directly. |
|
|
122
190
|
|
|
123
|
-
|
|
191
|
+
**Platform detection for `"intent"` mode:** iOS and Android are inferred from the user agent on mobile. On ambiguous desktop UAs the user is shown a picker — **Continue with Android** or **Continue with iPhone / iPad**.
|
|
124
192
|
|
|
125
|
-
|
|
193
|
+
> **QA only:** `_dangerouslyForceIntentPlatform: "android" | "ios"` bypasses detection. Do not use in production.
|
|
126
194
|
|
|
127
|
-
|
|
195
|
+
---
|
|
128
196
|
|
|
129
|
-
|
|
197
|
+
## Callbacks
|
|
130
198
|
|
|
131
|
-
|
|
199
|
+
| Callback | Fires when |
|
|
200
|
+
|----------|-----------|
|
|
201
|
+
| `onFlowEvent(evt)` | On every phase transition (see event names below) |
|
|
202
|
+
| `onSuccess(result)` | Terminal success — receives `{ txn_id?, status, raw }` |
|
|
203
|
+
| `onError(err)` | Agreement load failure or unrecoverable error |
|
|
204
|
+
| `onRequestClose()` | User dismissed via **×**, **Cancel**, or **Done** |
|
|
132
205
|
|
|
133
|
-
|
|
134
|
-
2. They invite your npm user with a role that can **publish** (e.g. member with publish, or owner).
|
|
135
|
-
3. You accept the invite, then run `npm login` and `npm publish --access public` again.
|
|
206
|
+
### `FlowEvent` values
|
|
136
207
|
|
|
137
|
-
|
|
208
|
+
```
|
|
209
|
+
agreement-loading → agreement-ready → txn-initiating → txn-created
|
|
210
|
+
→ handoff-loading → handoff-ready → polling → completed | failed | cancelled
|
|
211
|
+
```
|
|
138
212
|
|
|
139
|
-
|
|
140
|
-
2. Bump the version if you already published under another name, then `npm publish --access public`.
|
|
213
|
+
### Post-success helpers
|
|
141
214
|
|
|
142
|
-
|
|
215
|
+
```ts
|
|
216
|
+
import {
|
|
217
|
+
normalizeTxnStatusPayload,
|
|
218
|
+
extractDemographicData,
|
|
219
|
+
demographicAccordionSections,
|
|
220
|
+
} from "@snapkyc-ooru/consent-handoff-react";
|
|
221
|
+
```
|
|
143
222
|
|
|
144
|
-
|
|
223
|
+
---
|
|
145
224
|
|
|
146
|
-
|
|
225
|
+
## Theme
|
|
147
226
|
|
|
148
|
-
|
|
227
|
+
All overrides are optional. Unspecified keys fall back to the built-in warm default (orange primary, cream accents). Setting `primary` alone automatically sets `borderActive` unless you override it separately.
|
|
149
228
|
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
229
|
+
```tsx
|
|
230
|
+
import {
|
|
231
|
+
DEFAULT_SNAP_CONSENT_THEME,
|
|
232
|
+
resolveSnapConsentTheme,
|
|
233
|
+
type SnapConsentHandoffTheme,
|
|
234
|
+
} from "@snapkyc-ooru/consent-handoff-react";
|
|
235
|
+
|
|
236
|
+
const brand: Partial<SnapConsentHandoffTheme> = {
|
|
237
|
+
primary: "#0f766e",
|
|
238
|
+
surfaceMuted: "#f0fdfa",
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
<SnapConsentHandoffFlow {...props} theme={brand} />;
|
|
154
242
|
```
|
|
155
243
|
|
|
244
|
+
| Token | Controls |
|
|
245
|
+
|-------|----------|
|
|
246
|
+
| `primary` | Brand accent — **I Agree** / **Done**, checkboxes, active borders |
|
|
247
|
+
| `onPrimary` | Text on primary buttons |
|
|
248
|
+
| `canvas` | Sheet background |
|
|
249
|
+
| `surface` / `surfaceMuted` | Card surfaces (unselected / selected rows, notice) |
|
|
250
|
+
| `text` / `textMuted` | Body and secondary text |
|
|
251
|
+
| `border` / `borderActive` | Default and emphasized borders |
|
|
252
|
+
| `modalOverlay` | Modal scrim |
|
|
253
|
+
| `danger` | Error text |
|
|
254
|
+
| `success` | Success hero and status checkmarks |
|
|
255
|
+
| `successStatusSurface` | Status summary card on success |
|
|
256
|
+
| `successPurposesSurface` | Confirmed purposes panel |
|
|
257
|
+
| `successAccordionBorder` | Demographic accordion borders |
|
|
258
|
+
| `radius` / `fontFamily` / `gap` / `maxWidth` / `modalZ` | Layout and typography |
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
## Props reference
|
|
263
|
+
|
|
264
|
+
### Required props
|
|
265
|
+
|
|
266
|
+
| Prop | Type | Description |
|
|
267
|
+
|------|------|-------------|
|
|
268
|
+
| `integrationToken` | `string` | `Authorization: Token …` sent on every SDK request |
|
|
269
|
+
| `rpContext.reference_id` | `string` | Sent as `rp_context.ref_id` when initiating a transaction |
|
|
270
|
+
| `handoffMode` | `"qr" \| "intent"` | Handoff delivery mechanism |
|
|
271
|
+
|
|
272
|
+
### Optional props
|
|
273
|
+
|
|
274
|
+
| Prop | Type | Default | Description |
|
|
275
|
+
|------|------|---------|-------------|
|
|
276
|
+
| `rpContext.session_id` | `string` | — | Optional correlation metadata for your backend |
|
|
277
|
+
| `acceptLanguage` | `string` | — | `Accept-Language` header + agreement locale preference |
|
|
278
|
+
| `agreementId` | `string` | — | Reserved; agreement is implied by the integration token |
|
|
279
|
+
| `qrRequestBody` | `{ size?: number }` | — | Extra QR params; `txn_id` is always injected by SDK |
|
|
280
|
+
| `pollIntervalMs` | `number` | `1500` | Polling interval in milliseconds |
|
|
281
|
+
| `pollDeadlineMs` | `number` | `120000` | Polling timeout in milliseconds |
|
|
282
|
+
| `displayMode` | `"modal" \| "inline"` | `"modal"` | Modal (with scrim) or inline sheet |
|
|
283
|
+
| `title` | `string` | Agreement name | Dialog heading |
|
|
284
|
+
| `theme` | `Partial<SnapConsentHandoffTheme>` | Built-in warm default | Visual overrides |
|
|
285
|
+
| `intentPlatformOverride` | `"android" \| "ios"` | — | Force platform when already known |
|
|
286
|
+
| `userAgent` | `string` | — | Override UA for tests or SSR |
|
|
287
|
+
| `onFlowEvent` | `(evt: FlowEvent) => void` | — | Phase transition callback |
|
|
288
|
+
| `onSuccess` | `(result: CompletionResult) => void` | — | Terminal success callback |
|
|
289
|
+
| `onError` | `(err: unknown) => void` | — | Error callback |
|
|
290
|
+
| `onRequestClose` | `() => void` | — | Dismiss callback (required for host-controlled hide) |
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## Security — credential hygiene
|
|
295
|
+
|
|
296
|
+
`integrationToken` is a **bearer credential that lives in the browser.** Follow these rules:
|
|
297
|
+
|
|
298
|
+
- ✅ Mint **short-lived tokens** on your backend after the user is authenticated
|
|
299
|
+
- ✅ Deliver the token to the client just before calling `openSnapConsentHandoffFlow`
|
|
300
|
+
- ❌ Never embed long-lived secrets in static bundles or environment variables shipped to the browser
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
## HTTP contract
|
|
305
|
+
|
|
306
|
+
All route assumptions are documented in [`docs/HTTP_CONTRACT.md`](docs/HTTP_CONTRACT.md).
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
156
310
|
## License
|
|
157
311
|
|
|
158
|
-
MIT
|
|
312
|
+
[MIT](LICENSE)
|