@myazahq/kyc-sdk-react-native 3.0.0 → 3.1.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.
@@ -5,6 +5,7 @@ import { fillTokens } from '../../utils/tokens';
5
5
  import { hasEmailVerificationStep, hasPhoneVerificationStep } from '../../config/contact';
6
6
  import { hasActiveQuestionnaire } from '../../config/questionnaire';
7
7
  import { hasProofOfAddressStep } from '../../config/proofOfAddress';
8
+ import { mayAskSupportingDocuments } from '../../config/supportingDocuments';
8
9
  import { hasAddressCollectionStep } from '../../config/addressCollection';
9
10
  import {
10
11
  hasApplicantVerification,
@@ -70,11 +71,17 @@ const SCOPE_DESCRIPTIONS: Record<string, string> = {
70
71
  'We need to re-confirm the email address and phone number on your account. This takes a minute and is secure.',
71
72
  };
72
73
 
74
+ // The address scope's bullets are NOT a fixed pair: it verifies an address by
75
+ // the pin, by a document, or by both, so promising a map on a flow that only
76
+ // asks for a document is a promise the flow never keeps. Gated below on the
77
+ // same step-order predicate the flow itself walks; the document's own bullet is
78
+ // appended by the shared post-capture block, like every other flow's.
79
+ const ADDRESS_PIN_BULLETS: ConsentProcessStep[] = [
80
+ { icon: 'map-pin-house', label: 'Pin your home address on a map' },
81
+ { icon: 'badge-check', label: 'Confirm the details only you can know' },
82
+ ];
83
+
73
84
  const SCOPE_BULLETS: Record<string, ConsentProcessStep[]> = {
74
- address: [
75
- { icon: 'map-pin-house', label: 'Pin your home address on a map' },
76
- { icon: 'badge-check', label: 'Confirm the details only you can know' },
77
- ],
78
85
  'biometric-authentication': [
79
86
  { icon: 'scan-face', label: 'Take a quick selfie with liveness checks' },
80
87
  { icon: 'badge-check', label: 'We match it against your enrolled face' },
@@ -123,7 +130,11 @@ export function buildConsentModel(config: MyazaKYCConfig): ConsentModel {
123
130
  : scope
124
131
  ? // COPY the catalogue entry: pushing below would otherwise mutate the
125
132
  // shared constant, appending one more bullet per re-render.
126
- [...(SCOPE_BULLETS[scope] ?? [])]
133
+ scope === 'address'
134
+ ? hasAddressCollectionStep(config.addressCollection)
135
+ ? [...ADDRESS_PIN_BULLETS]
136
+ : []
137
+ : [...(SCOPE_BULLETS[scope] ?? [])]
127
138
  : [
128
139
  { icon: 'badge-check', label: 'Verify your government-issued ID' },
129
140
  { icon: 'user', label: 'Collect basic personal information' },
@@ -153,6 +164,12 @@ export function buildConsentModel(config: MyazaKYCConfig): ConsentModel {
153
164
  // Post-capture features, in the order the flow runs them. Each is gated on
154
165
  // the flow that actually asks for it, and skipped where a scope's own
155
166
  // catalogue bullet already covers the same step.
167
+ // `mayAsk`, NOT the step-order predicate: that one resolves against the
168
+ // verified IDs and consent runs before an ID is picked, so every scoped
169
+ // document would answer "nothing to ask for" and go undisclosed.
170
+ if (!isBusiness && mayAskSupportingDocuments(config.supportingDocuments)) {
171
+ steps.push({ icon: 'file-text', label: 'Upload supporting documents' });
172
+ }
156
173
  if (!isBusiness && hasProofOfAddressStep(config.proofOfAddress)) {
157
174
  steps.push({ icon: 'file-text', label: 'Upload a proof of address document' });
158
175
  }
@@ -1,5 +1,5 @@
1
1
  import React, { useEffect, useRef } from 'react';
2
- import Svg, { Circle } from 'react-native-svg';
2
+ import Svg, { Circle, G } from 'react-native-svg';
3
3
  import { advanceTarget, easeToward, mixHex } from '../../lib/captureRing';
4
4
 
5
5
  // A single line traced around the camera circle's edge for the length of the
@@ -69,23 +69,37 @@ export function CaptureRing({
69
69
  style={{ position: 'absolute', top: 0, left: 0 }}
70
70
  width={size}
71
71
  height={size}
72
- // Static origin, not motion: the arc starts at twelve o'clock.
73
- rotation={-90}
74
- originX={size / 2}
75
- originY={size / 2}
76
72
  >
77
- <Circle
78
- ref={ref}
79
- cx={size / 2}
80
- cy={size / 2}
81
- r={r}
82
- fill="none"
83
- stroke={color}
84
- strokeWidth={STROKE}
85
- strokeLinecap="butt"
86
- strokeDasharray={[circumference]}
87
- strokeDashoffset={circumference}
88
- />
73
+ {/* Static origin, not motion: the arc starts AND closes at twelve
74
+ o'clock, matching Flutter's `drawArc(rect, -pi / 2, ...)`.
75
+
76
+ The rotation sits on a <G>, not on the root <Svg>. react-native-svg
77
+ silently drops transform props on the root: its render applies a
78
+ transform only `if (transform)` — a `rotation`/`originX`/`originY`
79
+ triple leaves that undefined, so the branch never runs — and the
80
+ inner group it wraps children in is built from style/fill/stroke
81
+ props alone, so nothing forwards them there either. `SvgProps
82
+ extends GProps`, so the compiler accepts it and the arc quietly
83
+ starts at three o'clock, which is where the web SDK's CSS
84
+ `-rotate-90` would have put it had CSS applied here.
85
+
86
+ It is also deliberately not on the <Circle>: that node takes a
87
+ setNativeProps write every frame, and the transform has no business
88
+ sharing a node with the animation. */}
89
+ <G rotation={-90} originX={size / 2} originY={size / 2}>
90
+ <Circle
91
+ ref={ref}
92
+ cx={size / 2}
93
+ cy={size / 2}
94
+ r={r}
95
+ fill="none"
96
+ stroke={color}
97
+ strokeWidth={STROKE}
98
+ strokeLinecap="butt"
99
+ strokeDasharray={[circumference]}
100
+ strokeDashoffset={circumference}
101
+ />
102
+ </G>
89
103
  </Svg>
90
104
  );
91
105
  }
@@ -0,0 +1,126 @@
1
+ import React from 'react';
2
+ import { View } from 'react-native';
3
+
4
+ import { radius, spacing } from '../config/theme';
5
+ import { useTheme } from '../components/runtime';
6
+ import { MyazaText } from '../components/Typography';
7
+ import { Icon } from '../components/Icon';
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // The pieces of a supporting-document card.
11
+ //
12
+ // Split out of the card itself only for the file-length rule; they are that
13
+ // card's own furniture and nothing else builds them. The Flutter SDK splits
14
+ // the same three into supporting_document_parts.dart, for the same reason.
15
+ // ---------------------------------------------------------------------------
16
+
17
+ const MARKER = 28;
18
+
19
+ /** A count while the document is outstanding, and a state anybody can read once
20
+ * it is not. One document needs no number, so it wears a document glyph. */
21
+ export function DocumentMarker({
22
+ done,
23
+ position,
24
+ total,
25
+ }: {
26
+ done: boolean;
27
+ position: number;
28
+ total: number;
29
+ }): React.ReactElement {
30
+ const { colors } = useTheme();
31
+ return (
32
+ <View
33
+ style={{
34
+ width: MARKER,
35
+ height: MARKER,
36
+ borderRadius: radius.full,
37
+ alignItems: 'center',
38
+ justifyContent: 'center',
39
+ backgroundColor: done ? colors.primary : colors.primary100,
40
+ }}
41
+ >
42
+ {done ? (
43
+ <Icon name="check" size={16} color={colors.onPrimary} />
44
+ ) : total > 1 ? (
45
+ <MyazaText variant="bodySmall" color={colors.primary} style={{ fontWeight: '700' }}>
46
+ {String(position)}
47
+ </MyazaText>
48
+ ) : (
49
+ <Icon name="file-text" size={14} color={colors.primary} />
50
+ )}
51
+ </View>
52
+ );
53
+ }
54
+
55
+ /** Required or optional, in a word as well as a colour. */
56
+ export function DocumentStatePill({ required }: { required: boolean }): React.ReactElement {
57
+ const { colors } = useTheme();
58
+ return (
59
+ <View
60
+ style={{
61
+ paddingHorizontal: spacing.sm,
62
+ paddingVertical: 2,
63
+ borderRadius: radius.full,
64
+ backgroundColor: required ? colors.errorBg : colors.background,
65
+ }}
66
+ >
67
+ <MyazaText
68
+ variant="bodySmall"
69
+ color={required ? colors.error : colors.textSecondary}
70
+ style={{ fontWeight: '600' }}
71
+ >
72
+ {required ? 'Required' : 'Optional'}
73
+ </MyazaText>
74
+ </View>
75
+ );
76
+ }
77
+
78
+ /** What the server will take off this document, named as the author named it. */
79
+ export function DocumentReads({ reads }: { reads: readonly string[] }): React.ReactElement {
80
+ const { colors } = useTheme();
81
+ return (
82
+ <View
83
+ style={{
84
+ padding: spacing.sm + 4,
85
+ borderRadius: radius.sm,
86
+ backgroundColor: colors.background,
87
+ }}
88
+ >
89
+ <MyazaText
90
+ variant="bodySmall"
91
+ color={colors.textSecondary}
92
+ style={{ fontWeight: '600', letterSpacing: 0.5 }}
93
+ >
94
+ WHAT WE READ FROM IT
95
+ </MyazaText>
96
+ <View
97
+ style={{
98
+ marginTop: spacing.sm,
99
+ flexDirection: 'row',
100
+ flexWrap: 'wrap',
101
+ gap: spacing.xs + 2,
102
+ }}
103
+ >
104
+ {reads.map((read) => (
105
+ <View
106
+ key={read}
107
+ style={{
108
+ flexDirection: 'row',
109
+ alignItems: 'center',
110
+ paddingHorizontal: spacing.sm,
111
+ paddingVertical: 4,
112
+ borderRadius: radius.full,
113
+ borderWidth: 1,
114
+ borderColor: colors.border,
115
+ backgroundColor: colors.backgroundSecondary,
116
+ }}
117
+ >
118
+ <Icon name="check" size={12} color={colors.primary} />
119
+ <View style={{ width: 4 }} />
120
+ <MyazaText variant="bodySmall">{read}</MyazaText>
121
+ </View>
122
+ ))}
123
+ </View>
124
+ </View>
125
+ );
126
+ }
@@ -14,19 +14,30 @@ import { loadDocumentPicker } from '../services/documentPicker';
14
14
  // the refusal names the file that was chosen (images 5 MB, PDFs 15 MB).
15
15
  // ---------------------------------------------------------------------------
16
16
 
17
- export type AttachBusinessDocument = (
18
- slot: ResolvedBusinessDocumentType,
17
+ /**
18
+ * The minimum a slot must carry for this hook: it reads the label for error
19
+ * copy and hands the slot straight back to the caller's uploader. Generalised
20
+ * so the supporting-documents step shares this picker rather than owning a
21
+ * second copy of three file sources and their size/MIME rules.
22
+ */
23
+ export interface AttachableSlot {
24
+ key: string;
25
+ label: string;
26
+ }
27
+
28
+ export type AttachBusinessDocument<S extends AttachableSlot = ResolvedBusinessDocumentType> = (
29
+ slot: S,
19
30
  uri: string,
20
31
  mimeType: string | undefined,
21
32
  name: string,
22
33
  ) => Promise<void>;
23
34
 
24
- type Pick = (slot: ResolvedBusinessDocumentType) => Promise<void>;
35
+ type Pick<S extends AttachableSlot> = (slot: S) => Promise<void>;
25
36
 
26
- export function useBusinessDocumentAttach(
27
- attach: AttachBusinessDocument,
37
+ export function useBusinessDocumentAttach<S extends AttachableSlot = ResolvedBusinessDocumentType>(
38
+ attach: AttachBusinessDocument<S>,
28
39
  setError: (message: string) => void,
29
- ): { takePhoto: Pick; choosePhoto: Pick; chooseFile: Pick } {
40
+ ): { takePhoto: Pick<S>; choosePhoto: Pick<S>; chooseFile: Pick<S> } {
30
41
  const tooLarge = useCallback(
31
42
  (mime: string | undefined, size: number | undefined): boolean => {
32
43
  const message = uploadSizeError(mime, size);
@@ -36,7 +47,7 @@ export function useBusinessDocumentAttach(
36
47
  [setError],
37
48
  );
38
49
 
39
- const takePhoto = useCallback<Pick>(
50
+ const takePhoto = useCallback<Pick<S>>(
40
51
  async (slot) => {
41
52
  const permission = await ImagePicker.requestCameraPermissionsAsync();
42
53
  if (!permission.granted) {
@@ -53,7 +64,7 @@ export function useBusinessDocumentAttach(
53
64
  [attach, setError, tooLarge],
54
65
  );
55
66
 
56
- const choosePhoto = useCallback<Pick>(
67
+ const choosePhoto = useCallback<Pick<S>>(
57
68
  async (slot) => {
58
69
  const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ['images'], quality: 1 });
59
70
  const asset = result.canceled ? undefined : result.assets[0];
@@ -63,7 +74,7 @@ export function useBusinessDocumentAttach(
63
74
  [attach, tooLarge],
64
75
  );
65
76
 
66
- const chooseFile = useCallback<Pick>(
77
+ const chooseFile = useCallback<Pick<S>>(
67
78
  async (slot) => {
68
79
  const picker = loadDocumentPicker();
69
80
  if (!picker) {
@@ -24,6 +24,9 @@ export type MediaUploadType =
24
24
  // downloaded statement, and company paperwork is almost always a scan.
25
25
  | 'proof_of_address'
26
26
  | 'business_document'
27
+ // Artefacts held on file, named by the organisation — not the
28
+ // identity evidence the verification is decided on. Images + PDF.
29
+ | 'supporting_document'
27
30
  // Address Intelligence door / premises photo (image only).
28
31
  | 'address_photo';
29
32
 
@@ -15,7 +15,7 @@ export type DeviceType = 'mobile' | 'tablet' | 'desktop' | 'unknown';
15
15
  * Single source of truth for the SDK version — also used by `services/api.ts`
16
16
  * for the `X-SDK-Version` header. Keep in sync with `package.json`.
17
17
  */
18
- export const SDK_VERSION = '3.0.0';
18
+ export const SDK_VERSION = '3.1.0';
19
19
 
20
20
  export interface ReactNativeDeviceMetadata {
21
21
  sdkType: 'react-native';
@@ -8,6 +8,7 @@
8
8
  // ---------------------------------------------------------------------------
9
9
 
10
10
  import { configScope } from '../lib/scope';
11
+ import { hasSupportingDocumentsStep, verifiedIdsFor } from '../config/supportingDocuments';
11
12
  import { requiresDocumentCapture, supportsNfcChip } from '../config/idTypes';
12
13
  import {
13
14
  buildStepOrder,
@@ -83,6 +84,16 @@ export function stepOrderOptions(state: KycState): StepOrderOptions {
83
84
  hasEmailVerification: config.emailVerification?.enabled === true,
84
85
  hasPhoneVerification: config.phoneVerification?.enabled === true,
85
86
  hasPoa: hasProofOfAddressStep(config.proofOfAddress),
87
+ // Resolved against the ID actually picked — that is what decides whether
88
+ // the step has anything to ask for.
89
+ hasSupportingDocuments: hasSupportingDocumentsStep(
90
+ config.supportingDocuments,
91
+ verifiedIdsFor({
92
+ country: state.selectedCountry ?? config.country,
93
+ idType: state.selectedIdType,
94
+ multiIdSlots: state.multiIdSlots,
95
+ }),
96
+ ),
86
97
  hasAddressCollection: hasAddressCollectionStep(config.addressCollection),
87
98
  // An absent search flag means no search SCREEN, never an error: the
88
99
  // applicant places the pin by hand, the fallback every address failure
@@ -126,6 +126,7 @@ export function createKycStore(
126
126
  contactChallenge: null,
127
127
  business: EMPTY_BUSINESS,
128
128
  businessApplication: EMPTY_BUSINESS_APPLICATION,
129
+ supportingDocuments: [],
129
130
  applicantKeyPersonId: null,
130
131
  keyPeopleInvites: [],
131
132
  captureIntegrity: null,
@@ -423,6 +424,20 @@ export function createKycStore(
423
424
  }));
424
425
  },
425
426
 
427
+ setSupportingDocument(doc) {
428
+ set((s) => ({
429
+ // One upload per document, like the business slots: re-uploading
430
+ // replaces rather than appends.
431
+ supportingDocuments: [...s.supportingDocuments.filter((d) => d.type !== doc.type), doc],
432
+ }));
433
+ },
434
+
435
+ removeSupportingDocument(type) {
436
+ set((s) => ({
437
+ supportingDocuments: s.supportingDocuments.filter((d) => d.type !== type),
438
+ }));
439
+ },
440
+
426
441
  removeBusinessDocument(type) {
427
442
  set((s) => ({
428
443
  businessApplication: {
@@ -90,6 +90,17 @@ export function restoreAttemptProgress(
90
90
  const s = store.getState();
91
91
  const d = (progress.data ?? {}) as Record<string, unknown>;
92
92
 
93
+ // Validate-and-drop: a snapshot written by an older build must degrade to
94
+ // restoring less, never to breaking the flow.
95
+ const restoredDocs = Array.isArray(d['supportingDocuments'])
96
+ ? (d['supportingDocuments'] as unknown[]).filter(
97
+ (doc): doc is { type: string; mediaId: string } =>
98
+ !!doc &&
99
+ typeof doc === 'object' &&
100
+ typeof (doc as { type?: unknown }).type === 'string' &&
101
+ typeof (doc as { mediaId?: unknown }).mediaId === 'string',
102
+ )
103
+ : null;
93
104
  const app = d['businessApplication'] as
94
105
  | (Partial<KycState['businessApplication']> & { keyPeople?: Array<Record<string, unknown>> })
95
106
  | undefined;
@@ -116,6 +127,7 @@ export function restoreAttemptProgress(
116
127
  ...(d['business'] && typeof d['business'] === 'object'
117
128
  ? { business: { ...s.business, ...(d['business'] as object) } }
118
129
  : {}),
130
+ ...(restoredDocs ? { supportingDocuments: restoredDocs } : {}),
119
131
  ...(app
120
132
  ? {
121
133
  businessApplication: {
@@ -180,6 +192,12 @@ export function progressFromState(s: ReturnType<KycStore['getState']>): Record<s
180
192
  idNumber: s.idNumber || undefined,
181
193
  business: s.business,
182
194
  businessApplication: s.businessApplication,
195
+ // The uploads without their preview names: a restored attempt shows the
196
+ // slot as uploaded, which is what the mediaId is for.
197
+ supportingDocuments:
198
+ s.supportingDocuments.length > 0
199
+ ? s.supportingDocuments.map((d) => ({ type: d.type, mediaId: d.mediaId }))
200
+ : undefined,
183
201
  contact: s.contact,
184
202
  questionnaireAnswers: s.questionnaireAnswers,
185
203
  address: s.address ?? undefined,
@@ -402,6 +402,24 @@ export interface KycState {
402
402
  business: BusinessState;
403
403
  /** The rest of the KYB application: people, documents, applicant role. */
404
404
  businessApplication: BusinessApplicationState;
405
+
406
+ /**
407
+ * Supporting documents the person uploaded — artefacts the org holds on
408
+ * file, not the identity evidence this verification is decided on.
409
+ * `fileName` is display only and never rides the wire or session progress.
410
+ */
411
+ supportingDocuments: Array<{
412
+ type: string;
413
+ mediaId: string;
414
+ fileName?: string;
415
+ /**
416
+ * Local URI of the picked image, for the card's thumbnail. Display only,
417
+ * like `fileName`: it never rides the wire or session progress, so a
418
+ * restored attempt shows the slot uploaded without a preview.
419
+ */
420
+ previewUri?: string;
421
+ isPdf?: boolean;
422
+ }>;
405
423
  /**
406
424
  * Set by a KYB submission that requires applicant verification — the
407
425
  * KeyPerson id the applicant's own individual check links back to.
@@ -500,6 +518,14 @@ export interface KycState {
500
518
  setUboUnidentifiable: (checked: boolean) => void;
501
519
  setBusinessDocument: (doc: BusinessDocumentUpload) => void;
502
520
  removeBusinessDocument: (type: string) => void;
521
+ setSupportingDocument: (doc: {
522
+ type: string;
523
+ mediaId: string;
524
+ fileName?: string;
525
+ previewUri?: string;
526
+ isPdf?: boolean;
527
+ }) => void;
528
+ removeSupportingDocument: (type: string) => void;
503
529
  setApplicant: (role: ApplicantRole, name: string, keyPersonIndex?: number | null) => void;
504
530
  setCaptureIntegrity: (integrity: CaptureIntegrity) => void;
505
531
  setMrzScan: (scan: MrzScan) => void;
@@ -99,6 +99,17 @@ export function buildVerifyRequest(
99
99
  : state.mediaIds,
100
100
  ...(state.config.workflowId ? { workflowId: state.config.workflowId } : {}),
101
101
  ...(state.poaDocumentType ? { proofOfAddressType: state.poaDocumentType } : {}),
102
+ // Supporting documents — artefacts held on file. The server validates them
103
+ // against the resolved workflow's request list and drops anything it did
104
+ // not ask for. File names are display only and never ride the wire.
105
+ ...(state.supportingDocuments.length > 0
106
+ ? {
107
+ supportingDocuments: state.supportingDocuments.map((d) => ({
108
+ type: d.type,
109
+ mediaId: d.mediaId,
110
+ })),
111
+ }
112
+ : {}),
102
113
  // The smart address, when the step gathered one. Sent on individual AND
103
114
  // business submissions (a KYB flow's pin is the business premises) — the
104
115
  // server validates it against the workflow either way.
@@ -22,6 +22,7 @@ import type {
22
22
  NfcConfig,
23
23
  PhoneVerificationConfig,
24
24
  ProofOfAddressConfig,
25
+ SupportingDocumentsConfig,
25
26
  QuestionnaireConfig,
26
27
  WorkflowCountry,
27
28
  MultiIdConfig,
@@ -57,6 +58,7 @@ export type KYCStep =
57
58
  | 'applicant-role'
58
59
  | 'liveness'
59
60
  | 'proof-of-address'
61
+ | 'supporting-documents'
60
62
  // The address flow, in order: find it (search) → confirm it (the PIN step,
61
63
  // which keeps the original 'address-collection' wire name so older session
62
64
  // progress restores cleanly) → show it (entrance photo) → commit it.
@@ -246,6 +248,14 @@ export interface MyazaKYCConfig<C extends SupportedCountry = SupportedCountry> {
246
248
  /** Proof-of-address document check, after capture. */
247
249
  proofOfAddress?: ProofOfAddressConfig;
248
250
 
251
+ /**
252
+ * Supporting documents: artefacts the org holds ON FILE, which are NOT the
253
+ * identity evidence the verification is decided on. The org names each one
254
+ * and writes the guidance under it; the result of any check never changes
255
+ * the verification's own status.
256
+ */
257
+ supportingDocuments?: SupportingDocumentsConfig;
258
+
249
259
  /** Address Intelligence: a map-pin smart address (+ optional door photo and
250
260
  * directions), corroborated server-side. KYC AND KYB (premises pin). */
251
261
  addressCollection?: AddressCollectionConfig;
@@ -24,6 +24,45 @@ export type PoaDocumentType =
24
24
  * server; read here only to word the step. */
25
25
  export type PoaNameRule = 'required' | 'optional' | 'off';
26
26
 
27
+ /**
28
+ * One requested supporting document. There is no catalogue: the organisation
29
+ * names its own, so the SDK renders the title and guidance the workflow sent.
30
+ */
31
+ export interface SupportingDocumentRequest {
32
+ /** The organisation's own slug — the wire `type` this upload submits as. */
33
+ key: string;
34
+ /** The title the applicant reads. A document without one is not asked for. */
35
+ label?: string;
36
+ /** Guidance under the slot: which document, and what it has to show. */
37
+ description?: string;
38
+ /** Server-side only: what the document is corroborated against. */
39
+ checks?: Array<'id_number' | 'name'>;
40
+ /**
41
+ * The named values the server will read off it. The SDK reads the NAMES
42
+ * alone, to tell the applicant what the document is being taken for; the
43
+ * reading itself is entirely server-side.
44
+ */
45
+ fields?: Array<{ key: string; label?: string }>;
46
+ required?: boolean;
47
+ /** Which verified IDs it is asked for, as "CC/idType". Absent = every ID. */
48
+ idTypes?: string[];
49
+ /**
50
+ * Offer it on every flow, whatever ID was verified, so `idTypes` decides only
51
+ * who MUST provide it rather than who sees the slot. Absent = the scope hides
52
+ * it from everybody else, which is the default.
53
+ */
54
+ alwaysAsk?: boolean;
55
+ }
56
+
57
+ export interface SupportingDocumentsConfig {
58
+ enabled?: boolean;
59
+ types?: SupportingDocumentRequest[];
60
+ /** Server-side only. */
61
+ retentionDays?: number;
62
+ /** Server-side only. */
63
+ returnedData?: string[];
64
+ }
65
+
27
66
  export interface ProofOfAddressConfig {
28
67
  /** Adds the Proof of Address step (after capture, before the questionnaire). */
29
68
  enabled?: boolean;