@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.
Files changed (2) hide show
  1. package/README.md +257 -103
  2. 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
- React + TypeScript SDK for the **integrations agreement → transaction initiate → QR _or_ intentstatus polling** flow. `API_ORIGIN` is fixed in [`src/config.ts`](src/config.ts) (currently `http://127.0.0.1:8000`). Relying parties pass a single **`integrationToken`** (`Authorization: Token …`). See [`docs/HTTP_CONTRACT.md`](docs/HTTP_CONTRACT.md) for route assumptions.
3
+ > **KYC consent & identity handoff flow for React** agreement → consent → QR / deeplink → polling verified.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@snapkyc-ooru/consent-handoff-react)](https://www.npmjs.com/package/@snapkyc-ooru/consent-handoff-react)
6
+ [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
7
+ [![peer: React ≥ 17](https://img.shields.io/badge/peer-React%20%E2%89%A5%2017-61dafb)](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
- Design tokens are scoped to the SDK root (`.snap-consent-handoff`). If you previously overrode appearance by targeting **`:root`**, switch to the **`theme` prop** (see below) or to selectors under `.snap-consent-handoff` so you do not affect the host application.
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
- ## Theme (optional)
46
+ ---
20
47
 
21
- Pass **`theme`** with any subset of fields; the rest use the built-in warm default palette (orange primary, cream accents, light canvas). Changing **`primary`** alone updates accents and the default active border color unless you set **`borderActive`** explicitly.
48
+ ## The 5-step flow
22
49
 
23
- | Field | Role |
24
- |-------|------|
25
- | `primary` | Brand color primary buttons, checkboxes, selected borders |
26
- | `onPrimary` | Text on primary buttons |
27
- | `canvas` | Sheet / panel background |
28
- | `surface` | Default “card” surfaces (e.g. unselected purpose rows) |
29
- | `surfaceMuted` | Agreement notice and selected purpose row background |
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
- ```tsx
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
- const brand: Partial<SnapConsentHandoffTheme> = {
50
- primary: "#0f766e",
51
- borderActive: "#0f766e",
52
- surfaceMuted: "#f0fdfa",
53
- };
60
+ ---
54
61
 
55
- <SnapConsentHandoffFlow {...props} theme={brand} />;
56
- ```
62
+ ## Quick start
57
63
 
58
- `DEFAULT_SNAP_CONSENT_THEME` and `resolveSnapConsentTheme` are exported if you need to merge or inspect defaults in app code.
64
+ ### Option A Imperative modal (recommended)
59
65
 
60
- ## Imperative opener (recommended for modals)
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 { openSnapConsentHandoffFlow } from "@snapkyc/consent-handoff-react";
64
- import "@snapkyc/consent-handoff-react/styles.css";
65
-
66
- const { dispose } = openSnapConsentHandoffFlow({
67
- integrationToken,
68
- rpContext: { reference_id: user.id, session_id },
69
- /** Select exactly one post-initiation path — never both */
70
- handoffMode: "qr", // or "intent"
71
- /** Optional tweaks */
72
- pollIntervalMs: 1500,
73
- pollDeadlineMs: 120_000,
74
- onFlowEvent(e) {},
75
- onSuccess(result) {},
76
- onError(err) {},
77
- });
78
-
79
- // On route change / unmount caller context
80
- dispose();
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
- `openSnapConsentQrFlow` is a **deprecated alias** of the same function.
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
- ### Intent platform detection
125
+ > `openSnapConsentQrFlow` is a **deprecated alias** — use `openSnapConsentHandoffFlow` going forward.
86
126
 
87
- With `handoffMode: "intent"`, the SDK infers **`android`** vs **`ios`** from the browser UA. If ambiguous (typical desktop browser), users pick **Continue with Android** or **Continue with iPhone/iPad**. For automated tests only, set `_dangerouslyForceIntentPlatform`.
127
+ ---
88
128
 
89
- ## Declarative
129
+ ### Option B — Declarative component
90
130
 
91
- On the consent step, the primary action is labelled **I Agree** (with **Cancel** beside it to dismiss). After a successful verification, **Done** dismisses the flow the same way as the modal close control.
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 { SnapConsentHandoffFlow } from "@snapkyc/consent-handoff-react";
95
- import "@snapkyc/consent-handoff-react/styles.css";
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, session_id }}
100
- handoffMode="intent"
101
- displayMode="inline"
102
- />;
170
+ rpContext={{ reference_id: "USER-123" }}
171
+ handoffMode="qr"
172
+ displayMode="modal"
173
+ onRequestClose={() => setShowModal(false)}
174
+ />
103
175
  ```
104
176
 
105
- ## Props (summary)
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
- | Prop | Purpose |
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
- ## Credential hygiene
184
+ ## Handoff modes
120
185
 
121
- `integrationToken` is a bearer credential in the browser. Prefer short-lived minted tokens from your backend rather than embedding long-lived secrets in static bundles.
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
- ## Publishing / environments
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
- Bump `API_ORIGIN` when cutting staging/production builds (fork your publish pipeline per environment or maintain separate semver lines).
193
+ > **QA only:** `_dangerouslyForceIntentPlatform: "android" | "ios"` bypasses detection. Do not use in production.
126
194
 
127
- ### npm: `E404` — “Scope not found” (`@snapkyc/...`)
195
+ ---
128
196
 
129
- Scoped packages like **`@snapkyc/consent-handoff-react`** can only be published if the **scope exists on npm** and **your logged-in user is allowed to publish** to it.
197
+ ## Callbacks
130
198
 
131
- **Option A use the `snapkyc` org (intended for SnapKYC)**
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
- 1. An org owner creates **[`snapkyc` on npm](https://www.npmjs.com/org/create)** (if it does not exist yet).
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
- **Option B — publish under your own scope**
208
+ ```
209
+ agreement-loading → agreement-ready → txn-initiating → txn-created
210
+ → handoff-loading → handoff-ready → polling → completed | failed | cancelled
211
+ ```
138
212
 
139
- 1. Change the `"name"` field in `package.json` to a scope you control, e.g. `"@<your-npm-username>/consent-handoff-react"`.
140
- 2. Bump the version if you already published under another name, then `npm publish --access public`.
213
+ ### Post-success helpers
141
214
 
142
- **Option C — unscoped package**
215
+ ```ts
216
+ import {
217
+ normalizeTxnStatusPayload,
218
+ extractDemographicData,
219
+ demographicAccordionSections,
220
+ } from "@snapkyc-ooru/consent-handoff-react";
221
+ ```
143
222
 
144
- Use a unique unscoped name (e.g. `snapkyc-consent-handoff-react`) in `"name"` if you do not use npm orgs. Check name availability on [npmjs.com](https://www.npmjs.com/).
223
+ ---
145
224
 
146
- After any rename, update install instructions and all `import` paths for consumers.
225
+ ## Theme
147
226
 
148
- ## Develop
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
- ```bash
151
- npm install
152
- npm test
153
- npm run build
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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@snapkyc-ooru/consent-handoff-react",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "React SDK: agreement consent flow with QR or intent handoff + txn polling.",
5
5
  "license": "MIT",
6
6
  "type": "module",