@trustchex/react-native-sdk 1.500.2 → 1.501.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 (97) hide show
  1. package/android/src/main/java/com/trustchex/reactnativesdk/GeolocationModule.kt +159 -0
  2. package/android/src/main/java/com/trustchex/reactnativesdk/TrustchexSDKPackage.kt +12 -0
  3. package/ios/Permission/CoarseLocationProvider.swift +111 -0
  4. package/ios/Permission/PermissionModule.m +4 -0
  5. package/ios/Permission/PermissionModule.swift +21 -1
  6. package/lib/module/Screens/Dynamic/AddressCaptureScreen.js +230 -0
  7. package/lib/module/Screens/Dynamic/ContractAcceptanceScreen.js +12 -0
  8. package/lib/module/Screens/Dynamic/IdentityDocumentEIDScanningScreen.js +33 -10
  9. package/lib/module/Screens/Dynamic/IdentityDocumentScanningScreen.js +8 -10
  10. package/lib/module/Screens/Static/ResultScreen.js +74 -3
  11. package/lib/module/Shared/Components/EIDScanner.js +59 -3
  12. package/lib/module/Shared/Components/NavigationManager.js +6 -2
  13. package/lib/module/Shared/EIDReader/eidReader.js +52 -3
  14. package/lib/module/Shared/EIDReader/java/inputStream.js +6 -0
  15. package/lib/module/Shared/EIDReader/lds/icao/dg11File.js +60 -20
  16. package/lib/module/Shared/EIDReader/passiveAuth/der.js +93 -0
  17. package/lib/module/Shared/EIDReader/passiveAuth/integrity.js +71 -0
  18. package/lib/module/Shared/EIDReader/passiveAuth/sodParser.js +123 -0
  19. package/lib/module/Shared/Libs/chipVizMatch.js +93 -0
  20. package/lib/module/Shared/Libs/country-allow.js +16 -0
  21. package/lib/module/Shared/Libs/documentDataNormalizer.js +21 -11
  22. package/lib/module/Shared/Libs/geolocation.utils.js +77 -0
  23. package/lib/module/Shared/Libs/promise.utils.js +6 -0
  24. package/lib/module/Shared/Services/DataUploadService.js +18 -1
  25. package/lib/module/Translation/Resources/en.js +16 -0
  26. package/lib/module/Translation/Resources/tr.js +16 -0
  27. package/lib/module/Trustchex.js +4 -0
  28. package/lib/module/version.js +1 -1
  29. package/lib/typescript/src/Screens/Dynamic/AddressCaptureScreen.d.ts +9 -0
  30. package/lib/typescript/src/Screens/Dynamic/AddressCaptureScreen.d.ts.map +1 -0
  31. package/lib/typescript/src/Screens/Dynamic/ContractAcceptanceScreen.d.ts.map +1 -1
  32. package/lib/typescript/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.d.ts.map +1 -1
  33. package/lib/typescript/src/Screens/Dynamic/IdentityDocumentScanningScreen.d.ts.map +1 -1
  34. package/lib/typescript/src/Screens/Static/ResultScreen.d.ts.map +1 -1
  35. package/lib/typescript/src/Shared/Components/EIDScanner.d.ts +7 -1
  36. package/lib/typescript/src/Shared/Components/EIDScanner.d.ts.map +1 -1
  37. package/lib/typescript/src/Shared/Components/NavigationManager.d.ts.map +1 -1
  38. package/lib/typescript/src/Shared/EIDReader/eidReader.d.ts +14 -1
  39. package/lib/typescript/src/Shared/EIDReader/eidReader.d.ts.map +1 -1
  40. package/lib/typescript/src/Shared/EIDReader/java/inputStream.d.ts +3 -0
  41. package/lib/typescript/src/Shared/EIDReader/java/inputStream.d.ts.map +1 -1
  42. package/lib/typescript/src/Shared/EIDReader/lds/icao/dg11File.d.ts +20 -7
  43. package/lib/typescript/src/Shared/EIDReader/lds/icao/dg11File.d.ts.map +1 -1
  44. package/lib/typescript/src/Shared/EIDReader/passiveAuth/der.d.ts +31 -0
  45. package/lib/typescript/src/Shared/EIDReader/passiveAuth/der.d.ts.map +1 -0
  46. package/lib/typescript/src/Shared/EIDReader/passiveAuth/integrity.d.ts +31 -0
  47. package/lib/typescript/src/Shared/EIDReader/passiveAuth/integrity.d.ts.map +1 -0
  48. package/lib/typescript/src/Shared/EIDReader/passiveAuth/sodParser.d.ts +12 -0
  49. package/lib/typescript/src/Shared/EIDReader/passiveAuth/sodParser.d.ts.map +1 -0
  50. package/lib/typescript/src/Shared/Libs/chipVizMatch.d.ts +68 -0
  51. package/lib/typescript/src/Shared/Libs/chipVizMatch.d.ts.map +1 -0
  52. package/lib/typescript/src/Shared/Libs/country-allow.d.ts +10 -0
  53. package/lib/typescript/src/Shared/Libs/country-allow.d.ts.map +1 -0
  54. package/lib/typescript/src/Shared/Libs/documentDataNormalizer.d.ts.map +1 -1
  55. package/lib/typescript/src/Shared/Libs/geolocation.utils.d.ts +19 -0
  56. package/lib/typescript/src/Shared/Libs/geolocation.utils.d.ts.map +1 -0
  57. package/lib/typescript/src/Shared/Libs/promise.utils.d.ts.map +1 -1
  58. package/lib/typescript/src/Shared/Services/DataUploadService.d.ts.map +1 -1
  59. package/lib/typescript/src/Shared/Types/documentData.d.ts +7 -0
  60. package/lib/typescript/src/Shared/Types/documentData.d.ts.map +1 -1
  61. package/lib/typescript/src/Shared/Types/documentReadResult.d.ts +20 -0
  62. package/lib/typescript/src/Shared/Types/documentReadResult.d.ts.map +1 -1
  63. package/lib/typescript/src/Shared/Types/identificationInfo.d.ts +95 -2
  64. package/lib/typescript/src/Shared/Types/identificationInfo.d.ts.map +1 -1
  65. package/lib/typescript/src/Translation/Resources/en.d.ts +16 -0
  66. package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
  67. package/lib/typescript/src/Translation/Resources/tr.d.ts +16 -0
  68. package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
  69. package/lib/typescript/src/Trustchex.d.ts.map +1 -1
  70. package/lib/typescript/src/version.d.ts +1 -1
  71. package/package.json +1 -1
  72. package/src/Screens/Dynamic/AddressCaptureScreen.tsx +196 -0
  73. package/src/Screens/Dynamic/ContractAcceptanceScreen.tsx +12 -0
  74. package/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.tsx +46 -14
  75. package/src/Screens/Dynamic/IdentityDocumentScanningScreen.tsx +9 -11
  76. package/src/Screens/Static/ResultScreen.tsx +101 -6
  77. package/src/Shared/Components/EIDScanner.tsx +69 -2
  78. package/src/Shared/Components/NavigationManager.tsx +6 -0
  79. package/src/Shared/EIDReader/eidReader.ts +75 -2
  80. package/src/Shared/EIDReader/java/inputStream.ts +6 -0
  81. package/src/Shared/EIDReader/lds/icao/dg11File.ts +68 -22
  82. package/src/Shared/EIDReader/passiveAuth/der.ts +110 -0
  83. package/src/Shared/EIDReader/passiveAuth/integrity.ts +97 -0
  84. package/src/Shared/EIDReader/passiveAuth/sodParser.ts +142 -0
  85. package/src/Shared/Libs/chipVizMatch.ts +149 -0
  86. package/src/Shared/Libs/country-allow.ts +17 -0
  87. package/src/Shared/Libs/documentDataNormalizer.ts +24 -11
  88. package/src/Shared/Libs/geolocation.utils.ts +87 -0
  89. package/src/Shared/Libs/promise.utils.ts +11 -0
  90. package/src/Shared/Services/DataUploadService.ts +23 -0
  91. package/src/Shared/Types/documentData.ts +8 -0
  92. package/src/Shared/Types/documentReadResult.ts +20 -0
  93. package/src/Shared/Types/identificationInfo.ts +97 -2
  94. package/src/Translation/Resources/en.ts +21 -0
  95. package/src/Translation/Resources/tr.ts +21 -0
  96. package/src/Trustchex.tsx +5 -0
  97. package/src/version.ts +1 -1
@@ -0,0 +1,142 @@
1
+ import {
2
+ readNode,
3
+ readChildren,
4
+ decodeOid,
5
+ decodeInteger,
6
+ toHex,
7
+ type DerNode,
8
+ } from './der';
9
+
10
+ /**
11
+ * Parse EF.SOD (ICAO 9303-11 §5.1) to extract the LDSSecurityObject: the digest
12
+ * algorithm and the per-data-group hash list the chip's issuer signed. This is
13
+ * the input to the on-device integrity check (recompute each read DG's hash and
14
+ * compare). Signature verification + CSCA chaining is done server-side.
15
+ *
16
+ * Dependency-free DER walking so it is fully unit-testable from hex fixtures.
17
+ */
18
+
19
+ /** Map of OID → digest algorithm name used for DG hashing. */
20
+ const DIGEST_OIDS: Record<string, string> = {
21
+ '1.3.14.3.2.26': 'SHA-1',
22
+ '2.16.840.1.101.3.4.2.1': 'SHA-256',
23
+ '2.16.840.1.101.3.4.2.2': 'SHA-384',
24
+ '2.16.840.1.101.3.4.2.3': 'SHA-512',
25
+ '2.16.840.1.101.3.4.2.4': 'SHA-224',
26
+ };
27
+
28
+ const OID_SIGNED_DATA = '1.2.840.113549.1.7.2';
29
+
30
+ export interface SodSecurityObject {
31
+ /** Digest algorithm name, e.g. "SHA-256". */
32
+ digestAlgorithm: string;
33
+ /** Per-DG hashes from the SOD: dataGroupNumber → lowercase hex hash. */
34
+ dataGroupHashes: Record<number, string>;
35
+ }
36
+
37
+ const TAG_SEQUENCE = 0x10;
38
+ const TAG_OID = 0x06;
39
+ const TAG_OCTET_STRING = 0x04;
40
+ const TAG_INTEGER = 0x02;
41
+ const TAG_SET = 0x11;
42
+
43
+ /** Find the first child whose masked tag number equals `tagNumber`. */
44
+ function findChild(children: DerNode[], tagNumber: number): DerNode | undefined {
45
+ return children.find((c) => c.tagNumber === tagNumber);
46
+ }
47
+
48
+ /**
49
+ * Parse the EF.SOD bytes (with or without the outer 0x77 application wrapper).
50
+ * Throws on a malformed structure.
51
+ */
52
+ export function parseSod(sodBytes: Uint8Array): SodSecurityObject {
53
+ let root = readNode(sodBytes, 0);
54
+
55
+ // Unwrap the [APPLICATION 23] (0x77) wrapper if present.
56
+ if (root.tag === 0x77) {
57
+ root = readNode(root.content, 0);
58
+ }
59
+
60
+ // root = ContentInfo SEQUENCE { OID id-signedData, [0] SignedData }
61
+ if (root.tagNumber !== TAG_SEQUENCE) {
62
+ throw new Error('SOD: expected ContentInfo SEQUENCE');
63
+ }
64
+ const contentInfo = readChildren(root.content);
65
+ const ciOid = findChild(contentInfo, TAG_OID);
66
+ if (!ciOid || decodeOid(ciOid.content) !== OID_SIGNED_DATA) {
67
+ throw new Error('SOD: ContentInfo is not signedData');
68
+ }
69
+ // [0] explicit wrapper around SignedData.
70
+ const signedDataWrapper = contentInfo.find((c) => c.constructed && c.tag === 0xa0);
71
+ if (!signedDataWrapper) throw new Error('SOD: missing SignedData wrapper');
72
+ const signedData = readNode(signedDataWrapper.content, 0);
73
+ if (signedData.tagNumber !== TAG_SEQUENCE) {
74
+ throw new Error('SOD: SignedData not a SEQUENCE');
75
+ }
76
+
77
+ // SignedData = { version, digestAlgorithms SET, encapContentInfo SEQUENCE, ... }
78
+ const sdChildren = readChildren(signedData.content);
79
+ // encapContentInfo is the first SEQUENCE after the digestAlgorithms SET.
80
+ const setIdx = sdChildren.findIndex((c) => c.tagNumber === TAG_SET);
81
+ const encap = sdChildren
82
+ .slice(setIdx + 1)
83
+ .find((c) => c.tagNumber === TAG_SEQUENCE);
84
+ if (!encap) throw new Error('SOD: missing encapContentInfo');
85
+
86
+ // encapContentInfo = { eContentType OID, [0] eContent OCTET STRING }
87
+ const encapChildren = readChildren(encap.content);
88
+ const eContentWrapper = encapChildren.find((c) => c.constructed && c.tag === 0xa0);
89
+ if (!eContentWrapper) throw new Error('SOD: missing eContent');
90
+ const eContentOctet = readNode(eContentWrapper.content, 0);
91
+ if (eContentOctet.tagNumber !== TAG_OCTET_STRING) {
92
+ throw new Error('SOD: eContent not an OCTET STRING');
93
+ }
94
+
95
+ return parseLdsSecurityObject(eContentOctet.content);
96
+ }
97
+
98
+ /**
99
+ * Parse the LDSSecurityObject:
100
+ * SEQUENCE { version INTEGER, hashAlgorithm AlgorithmIdentifier,
101
+ * SEQUENCE OF DataGroupHash { number INTEGER, hash OCTET STRING } }
102
+ */
103
+ function parseLdsSecurityObject(bytes: Uint8Array): SodSecurityObject {
104
+ const lds = readNode(bytes, 0);
105
+ if (lds.tagNumber !== TAG_SEQUENCE) {
106
+ throw new Error('SOD: LDSSecurityObject not a SEQUENCE');
107
+ }
108
+ const children = readChildren(lds.content);
109
+
110
+ // hashAlgorithm AlgorithmIdentifier = SEQUENCE { OID, params? }
111
+ const algId = children.find((c) => c.tagNumber === TAG_SEQUENCE);
112
+ if (!algId) throw new Error('SOD: missing hashAlgorithm');
113
+ const algOidNode = findChild(readChildren(algId.content), TAG_OID);
114
+ if (!algOidNode) throw new Error('SOD: missing hashAlgorithm OID');
115
+ const digestAlgorithm = DIGEST_OIDS[decodeOid(algOidNode.content)];
116
+ if (!digestAlgorithm) {
117
+ throw new Error(`SOD: unsupported digest OID ${decodeOid(algOidNode.content)}`);
118
+ }
119
+
120
+ // dataGroupHashValues — the SEQUENCE OF after the hashAlgorithm.
121
+ const algIdx = children.indexOf(algId);
122
+ const hashSeq = children
123
+ .slice(algIdx + 1)
124
+ .find((c) => c.tagNumber === TAG_SEQUENCE);
125
+ if (!hashSeq) throw new Error('SOD: missing dataGroupHashValues');
126
+
127
+ const dataGroupHashes: Record<number, string> = {};
128
+ for (const dgh of readChildren(hashSeq.content)) {
129
+ if (dgh.tagNumber !== TAG_SEQUENCE) continue;
130
+ const parts = readChildren(dgh.content);
131
+ const numNode = parts.find((p) => p.tagNumber === TAG_INTEGER);
132
+ const hashNode = parts.find((p) => p.tagNumber === TAG_OCTET_STRING);
133
+ if (!numNode || !hashNode) continue;
134
+ dataGroupHashes[decodeInteger(numNode.content)] = toHex(hashNode.content);
135
+ }
136
+
137
+ if (Object.keys(dataGroupHashes).length === 0) {
138
+ throw new Error('SOD: no data-group hashes found');
139
+ }
140
+
141
+ return { digestAlgorithm, dataGroupHashes };
142
+ }
@@ -0,0 +1,149 @@
1
+ import type { DocumentData } from '../Types/documentData';
2
+
3
+ /**
4
+ * Verdict of comparing the NFC chip-read (DG1) identity data against the
5
+ * camera-OCR-read (printed / VIZ) identity data.
6
+ *
7
+ * Required by MASAK Tebliğ No. 32, Article 4/C(1)(b): for remote identity
8
+ * verification of non-Turkish nationals via NFC passport, the chip data must be
9
+ * verified — via NFC — to match the data printed on the passport. If it cannot
10
+ * be verified (`MISMATCH` or `UNVERIFIABLE`), no business relationship may be
11
+ * established through the remote flow.
12
+ *
13
+ * - `MATCH` — every identity-critical field is equal.
14
+ * - `MISMATCH` — at least one identity-critical field differs.
15
+ * - `UNVERIFIABLE` — one of the two sources is missing, so no comparison is possible.
16
+ */
17
+ export type ChipVizVerdict = 'MATCH' | 'MISMATCH' | 'UNVERIFIABLE';
18
+
19
+ /** Why a comparison came back `UNVERIFIABLE`. */
20
+ export type ChipVizUnverifiableReason =
21
+ | 'missing-ocr-source'
22
+ | 'missing-chip-source';
23
+
24
+ export interface ChipVizMatchResult {
25
+ verdict: ChipVizVerdict;
26
+ /**
27
+ * Fields that differed (empty on `MATCH` / `UNVERIFIABLE`). Used for
28
+ * diagnostics and server-side review — never surfaced to the user, to avoid
29
+ * coaching fraud.
30
+ */
31
+ mismatchedFields: Array<keyof DocumentData>;
32
+ /** Present only when `verdict === 'UNVERIFIABLE'`. */
33
+ reason?: ChipVizUnverifiableReason;
34
+ }
35
+
36
+ export interface ChipVizCompareOptions {
37
+ /**
38
+ * When `true`, a difference in the raw MRZ name (first/last) alone flips the
39
+ * verdict to `MISMATCH`. When `false` (default), name differences are
40
+ * recorded in `mismatchedFields` but do not by themselves cause a mismatch —
41
+ * a single-character OCR slip on a name should not hard-block on its own.
42
+ * The identity-critical fields below always drive the verdict regardless.
43
+ */
44
+ strictNames?: boolean;
45
+ /**
46
+ * Raw MRZ ASCII names (OCR-B, before Unicode/DG11 display conversion). The
47
+ * normalized `DocumentData.firstName/lastName` are overwritten with the
48
+ * display form in the read path, so the raw forms must be passed in
49
+ * explicitly for an apples-to-apples comparison.
50
+ */
51
+ chipName?: { first: string; last: string };
52
+ vizName?: { first: string; last: string };
53
+ }
54
+
55
+ /**
56
+ * Identity-critical fields. These are the ICAO check-digit-protected MRZ fields
57
+ * plus the core identity claim; any difference here is a hard mismatch.
58
+ */
59
+ const IDENTITY_CRITICAL_FIELDS: Array<keyof DocumentData> = [
60
+ 'documentNumber',
61
+ 'dateOfBirth',
62
+ 'dateOfExpiry',
63
+ 'nationality',
64
+ 'sex',
65
+ ];
66
+
67
+ /** Uppercase, strip `<` filler and surrounding whitespace. */
68
+ function normalizeField(value: string | null | undefined): string {
69
+ if (!value) return '';
70
+ return value.replace(/</g, '').trim().toUpperCase();
71
+ }
72
+
73
+ /** Collapse internal whitespace runs and strip `<` filler, for name comparison. */
74
+ function normalizeName(value: string | null | undefined): string {
75
+ if (!value) return '';
76
+ return value
77
+ .replace(/</g, ' ')
78
+ .replace(/\s+/g, ' ')
79
+ .trim()
80
+ .toUpperCase();
81
+ }
82
+
83
+ /**
84
+ * Compare the chip-read (DG1) document data against the OCR-read (printed/VIZ)
85
+ * document data and produce a match verdict.
86
+ *
87
+ * Both inputs are the normalized {@link DocumentData} shape, so they are
88
+ * directly comparable regardless of which read path produced them. Pass raw MRZ
89
+ * names via {@link ChipVizCompareOptions} — the `firstName`/`lastName` on
90
+ * `DocumentData` may already be the Unicode display form, which legitimately
91
+ * diverges between paths and would cause false mismatches.
92
+ *
93
+ * @param chip Chip-read data (from `normalizeFromMRZInfo`), or `undefined` if NFC did not complete.
94
+ * @param viz OCR-read data (from `normalizeFromMRZFields`), or `undefined` if no OCR step ran.
95
+ */
96
+ export function compareChipToViz(
97
+ chip: DocumentData | undefined,
98
+ viz: DocumentData | undefined,
99
+ options: ChipVizCompareOptions = {}
100
+ ): ChipVizMatchResult {
101
+ if (!chip) {
102
+ return {
103
+ verdict: 'UNVERIFIABLE',
104
+ mismatchedFields: [],
105
+ reason: 'missing-chip-source',
106
+ };
107
+ }
108
+ if (!viz) {
109
+ return {
110
+ verdict: 'UNVERIFIABLE',
111
+ mismatchedFields: [],
112
+ reason: 'missing-ocr-source',
113
+ };
114
+ }
115
+
116
+ const mismatchedFields: Array<keyof DocumentData> = [];
117
+
118
+ for (const field of IDENTITY_CRITICAL_FIELDS) {
119
+ if (normalizeField(chip[field] as string) !== normalizeField(viz[field] as string)) {
120
+ mismatchedFields.push(field);
121
+ }
122
+ }
123
+
124
+ // Names: compare the raw MRZ ASCII when provided, otherwise fall back to the
125
+ // (possibly display-form) DocumentData names. Recorded always; only flips the
126
+ // verdict when strictNames is enabled.
127
+ const chipFirst = options.chipName?.first ?? chip.firstName;
128
+ const chipLast = options.chipName?.last ?? chip.lastName;
129
+ const vizFirst = options.vizName?.first ?? viz.firstName;
130
+ const vizLast = options.vizName?.last ?? viz.lastName;
131
+
132
+ const firstDiffers = normalizeName(chipFirst) !== normalizeName(vizFirst);
133
+ const lastDiffers = normalizeName(chipLast) !== normalizeName(vizLast);
134
+
135
+ const nameMismatchedFields: Array<keyof DocumentData> = [];
136
+ if (firstDiffers) nameMismatchedFields.push('firstName');
137
+ if (lastDiffers) nameMismatchedFields.push('lastName');
138
+ mismatchedFields.push(...nameMismatchedFields);
139
+
140
+ // Identity-critical fields always drive the verdict; names only when strict.
141
+ const hardMismatch =
142
+ mismatchedFields.some((f) => IDENTITY_CRITICAL_FIELDS.includes(f)) ||
143
+ (options.strictNames === true && nameMismatchedFields.length > 0);
144
+
145
+ return {
146
+ verdict: hardMismatch ? 'MISMATCH' : 'MATCH',
147
+ mismatchedFields,
148
+ };
149
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Country allow-list check for a workflow step's `allowedCountries`.
3
+ *
4
+ * Semantics (mirrors the server engine): an empty or unset list means ALL
5
+ * countries are allowed; a non-empty list allows ONLY those ICAO-3 codes.
6
+ * Matching is case-insensitive. `allowedCountries` carries any ICAO-3 code
7
+ * (server-driven), so the client treats the values as opaque strings.
8
+ */
9
+ export function isCountryAllowed(
10
+ issuingState: string | null | undefined,
11
+ allowedCountries: string[] | null | undefined
12
+ ): boolean {
13
+ if (!allowedCountries || allowedCountries.length === 0) return true;
14
+ const code = issuingState?.toUpperCase();
15
+ if (!code) return false;
16
+ return allowedCountries.map((c) => c.toUpperCase()).includes(code);
17
+ }
@@ -3,26 +3,39 @@ import type { DocumentData } from '../Types/documentData';
3
3
  import type { MRZInfo } from '../EIDReader/lds/icao/mrzInfo';
4
4
 
5
5
  /**
6
- * Expand a 2-digit MRZ year to a 4-digit calendar year.
7
- * ICAO 9303: YY >= 30 → 19YY, YY < 30 → 20YY.
8
- * (Threshold 30 keeps all current valid documents in range.)
6
+ * Expand a 2-digit MRZ year to a 4-digit calendar year, using a century rule
7
+ * appropriate to the kind of date:
8
+ *
9
+ * - `'birth'`: a birth date is never in the future. Any YY greater than the
10
+ * current 2-digit year must be the previous century (e.g. in 2026, "85" →
11
+ * 1985, "20" → 2020). This is the ICAO-style sliding window pegged to "now".
12
+ * - `'expiry'`: a travel/ID document's expiry is recent-past-to-future and no
13
+ * document valid today expired in the 1900s, so expiry years are always 20YY
14
+ * (e.g. "36" → 2036, not 1936 — the previous fixed-threshold bug).
9
15
  */
10
- function expandYear(yy: string): string {
16
+ function expandYear(yy: string, kind: 'birth' | 'expiry'): string {
17
+ if (kind === 'expiry') return `20${yy}`;
11
18
  const n = parseInt(yy, 10);
12
- return n >= 30 ? `19${yy}` : `20${yy}`;
19
+ // Current 2-digit year as the sliding pivot (e.g. 26 in 2026). Years after it
20
+ // can't be a birth this century, so they belong to the 1900s.
21
+ const pivot = new Date().getFullYear() % 100;
22
+ return n > pivot ? `19${yy}` : `20${yy}`;
13
23
  }
14
24
 
15
25
  /**
16
26
  * Convert a YYMMDD MRZ date string to ISO 8601 YYYY-MM-DD.
17
27
  * Returns null for empty, null, or malformed input.
18
28
  */
19
- function mrzDateToISO(raw: string | null | undefined): string | null {
29
+ function mrzDateToISO(
30
+ raw: string | null | undefined,
31
+ kind: 'birth' | 'expiry'
32
+ ): string | null {
20
33
  if (!raw || raw.length !== 6) return null;
21
34
  const yy = raw.slice(0, 2);
22
35
  const mm = raw.slice(2, 4);
23
36
  const dd = raw.slice(4, 6);
24
37
  if (!/^\d{6}$/.test(raw)) return null;
25
- return `${expandYear(yy)}-${mm}-${dd}`;
38
+ return `${expandYear(yy, kind)}-${mm}-${dd}`;
26
39
  }
27
40
 
28
41
  /**
@@ -84,8 +97,8 @@ export function normalizeFromMRZInfo(mrz: MRZInfo): DocumentData {
84
97
  lastName: mrz.getPrimaryIdentifier().trim(),
85
98
  firstName: mrz.getSecondaryIdentifier().trim(),
86
99
  sex: normalizeSex(mrz.getGender().getStrCode()),
87
- dateOfBirth: mrzDateToISO(mrz.getDateOfBirth()),
88
- dateOfExpiry: mrzDateToISO(mrz.getDateOfExpiry()),
100
+ dateOfBirth: mrzDateToISO(mrz.getDateOfBirth(), 'birth'),
101
+ dateOfExpiry: mrzDateToISO(mrz.getDateOfExpiry(), 'expiry'),
89
102
  };
90
103
  }
91
104
 
@@ -102,7 +115,7 @@ export function normalizeFromMRZFields(fields: MRZFields): DocumentData {
102
115
  lastName: clean(fields.lastName) ?? '',
103
116
  firstName: clean(fields.firstName) ?? '',
104
117
  sex: normalizeSex(fields.sex),
105
- dateOfBirth: mrzDateToISO(fields.birthDate),
106
- dateOfExpiry: mrzDateToISO(fields.expirationDate),
118
+ dateOfBirth: mrzDateToISO(fields.birthDate, 'birth'),
119
+ dateOfExpiry: mrzDateToISO(fields.expirationDate, 'expiry'),
107
120
  };
108
121
  }
@@ -0,0 +1,87 @@
1
+ import { Platform, NativeModules, PermissionsAndroid } from 'react-native';
2
+ import { debugError } from './debug.utils';
3
+
4
+ /**
5
+ * Coarse geolocation capture for the MASAK Tebliğ No. 32 Article 4/C(1)(e)
6
+ * technical-data requirement. Invoked when a workflow step enables
7
+ * collectGeolocation.
8
+ *
9
+ * Implemented entirely with the platform's native location framework via the
10
+ * SDK's own native modules (iOS Core Location through PermissionModule, Android
11
+ * LocationManager through GeolocationModule) — no third-party module. The host
12
+ * app must declare the OS permission strings (iOS
13
+ * `NSLocationWhenInUseUsageDescription`, Android `ACCESS_COARSE_LOCATION`).
14
+ *
15
+ * Everything is fail-soft: if the native module is missing, services are off,
16
+ * permission is denied, or the fix times out, this returns null and the flow
17
+ * proceeds — the backend treats a missing geolocation as "not provided".
18
+ *
19
+ * @returns "lat,lng" rounded to ~city-block precision (~110 m), or null.
20
+ */
21
+ export async function captureCoarseGeolocation(
22
+ timeoutMs = 8000
23
+ ): Promise<string | null> {
24
+ try {
25
+ if (Platform.OS === 'android') {
26
+ const granted = await requestAndroidLocationPermission();
27
+ if (!granted) return null;
28
+ const mod = NativeModules.GeolocationModule;
29
+ if (!mod?.getCoarseLocation) return null;
30
+ return normalize(
31
+ await withJsBackstop(mod.getCoarseLocation(timeoutMs), timeoutMs)
32
+ );
33
+ }
34
+
35
+ // iOS: Core Location (via the existing PermissionModule) requests
36
+ // when-in-use authorization itself given the Info.plist usage string.
37
+ const mod = NativeModules.PermissionModule;
38
+ if (!mod?.getCoarseLocation) return null;
39
+ return normalize(
40
+ await withJsBackstop(mod.getCoarseLocation(timeoutMs), timeoutMs)
41
+ );
42
+ } catch (e) {
43
+ debugError('geolocation.utils', 'native capture failed', e);
44
+ return null;
45
+ }
46
+ }
47
+
48
+ function normalize(value: string | null | undefined): string | null {
49
+ if (typeof value !== 'string' || value.length === 0) return null;
50
+ return value;
51
+ }
52
+
53
+ /**
54
+ * JS-side backstop: the native side enforces its own timeout, but if a native
55
+ * regression left the promise unsettled, this resolves null so the flow can't
56
+ * hang. The margin keeps the native timeout authoritative on the happy path.
57
+ */
58
+ async function withJsBackstop(
59
+ native: Promise<string | null>,
60
+ timeoutMs: number
61
+ ): Promise<string | null> {
62
+ let timer: ReturnType<typeof setTimeout> | undefined;
63
+ const backstop = new Promise<string | null>((resolve) => {
64
+ timer = setTimeout(() => resolve(null), timeoutMs + 2000);
65
+ });
66
+ try {
67
+ return await Promise.race([native, backstop]);
68
+ } finally {
69
+ if (timer) clearTimeout(timer);
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Request coarse location permission on Android. The host app must declare
75
+ * ACCESS_COARSE_LOCATION in its manifest. iOS handles its own prompt through
76
+ * Core Location, so this is Android-only.
77
+ */
78
+ async function requestAndroidLocationPermission(): Promise<boolean> {
79
+ try {
80
+ const result = await PermissionsAndroid.request(
81
+ PermissionsAndroid.PERMISSIONS.ACCESS_COARSE_LOCATION
82
+ );
83
+ return result === PermissionsAndroid.RESULTS.GRANTED;
84
+ } catch {
85
+ return false;
86
+ }
87
+ }
@@ -18,6 +18,17 @@ const runWithRetry = async <T>(
18
18
  }
19
19
  return result;
20
20
  } catch (error) {
21
+ // Non-retryable errors (e.g. a server-instructed session termination,
22
+ // MASAK 4/C(1)(e)) must propagate immediately, with their flags intact —
23
+ // retrying can't help and would just delay surfacing the outcome.
24
+ if (
25
+ error &&
26
+ typeof error === 'object' &&
27
+ ((error as { sessionTerminated?: boolean }).sessionTerminated ||
28
+ (error as { nonRetryable?: boolean }).nonRetryable)
29
+ ) {
30
+ throw error;
31
+ }
21
32
  lastError = error;
22
33
  retries++;
23
34
  console.error(
@@ -74,12 +74,35 @@ export class DataUploadService {
74
74
  gender: this.getGenderEnumType(mrzFields.sex),
75
75
  number: mrzFields.documentNumber,
76
76
  country: mrzFields.issuingState,
77
+ // Raw ICAO nationality (for the MASAK 4/C(3) risky-country block and the
78
+ // server chip↔VIZ nationality comparison).
79
+ nationality: mrzFields.nationality,
77
80
  barcodeValue: scannedDocument.barcodeValue,
78
81
  personalNumber: mrzFields.personalNumber || mrzFields.optional1,
79
82
  birthDate: mrzUtils.convertMRZDateToISODate(mrzFields.birthDate),
80
83
  expiryDate: mrzUtils.convertMRZDateToISODate(mrzFields.expirationDate),
81
84
  dataSource: scannedDocument.dataSource,
82
85
  mrzText: scannedDocument.mrzText,
86
+ // Chip↔VIZ match (MASAK 4/C(1)(b)). Send the client verdict (advisory)
87
+ // plus the printed/VIZ MRZ comparison fields so the server can
88
+ // independently re-derive the authoritative verdict. Recorded for every
89
+ // EID scan as a security signal; absent on non-EID submissions.
90
+ chipVizMatch: scannedDocument.chipVizMatch,
91
+ vizMrz: scannedDocument.vizMrzFields
92
+ ? {
93
+ number: scannedDocument.vizMrzFields.documentNumber,
94
+ birthDate: mrzUtils.convertMRZDateToISODate(
95
+ scannedDocument.vizMrzFields.birthDate
96
+ ),
97
+ expiryDate: mrzUtils.convertMRZDateToISODate(
98
+ scannedDocument.vizMrzFields.expirationDate
99
+ ),
100
+ nationality: scannedDocument.vizMrzFields.nationality,
101
+ gender: this.getGenderEnumType(scannedDocument.vizMrzFields.sex),
102
+ name: scannedDocument.vizMrzFields.firstName,
103
+ surname: scannedDocument.vizMrzFields.lastName,
104
+ }
105
+ : undefined,
83
106
  };
84
107
 
85
108
  debugLog(
@@ -49,4 +49,12 @@ export interface DocumentData {
49
49
  * Null if the document does not encode it.
50
50
  */
51
51
  dateOfExpiry: string | null;
52
+
53
+ /**
54
+ * Holder's permanent address as read from the chip's DG11 (NFC path only),
55
+ * normalized to a comma-separated line. Null when the chip does not provide
56
+ * it (most passports) or on the OCR path. Used to pre-fill the address step
57
+ * (MASAK 4/C(1)(d)).
58
+ */
59
+ address?: string | null;
52
60
  }
@@ -1,4 +1,5 @@
1
1
  import type { DocumentData } from './documentData';
2
+ import type { ChipVizMatchResult } from '../Libs/chipVizMatch';
2
3
 
3
4
  /**
4
5
  * How the display name was derived from the raw MRZ ASCII name.
@@ -44,4 +45,23 @@ export interface DocumentReadResult {
44
45
  * face image was successfully obtained.
45
46
  */
46
47
  face?: DocumentFaceImage;
48
+ /**
49
+ * Result of comparing this chip read against a prior camera-OCR (printed/VIZ)
50
+ * read, when one is available. Present only on the NFC path in the
51
+ * MASAK 4/C remote-passport flow. The client verdict is advisory — the server
52
+ * re-derives the authoritative verdict from the uploaded fields.
53
+ */
54
+ chipVizMatch?: ChipVizMatchResult;
55
+ /**
56
+ * Passive Authentication result (ICAO 9303-11 §5.1), present only on the NFC
57
+ * path when the eID step enabled passiveAuth. `integrityStatus` is the
58
+ * on-device DG-hash check (MATCH/TAMPERED/INCOMPLETE); full signature + CSCA
59
+ * verification is performed server-side from the SOD.
60
+ */
61
+ passiveAuth?: {
62
+ integrityStatus: 'MATCH' | 'TAMPERED' | 'INCOMPLETE' | null;
63
+ /** Raw EF.SOD (base64) for server-side signature + CSCA chaining. */
64
+ sodBase64?: string | null;
65
+ error?: string;
66
+ };
47
67
  }