@myazahq/kyc-sdk-react-native 2.6.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.
- package/README.md +128 -4
- package/android/build.gradle +30 -16
- package/android/consumer-rules.pro +22 -0
- package/android/src/main/AndroidManifest.xml +17 -6
- package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaFaceDetector.kt +27 -72
- package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaTextRecognizer.kt +14 -0
- package/android/src/main/java/com/margelo/nitro/myazakyc/MlKitModelReadiness.kt +154 -0
- package/app.plugin.js +57 -2
- package/ios/HybridMyazaTextRecognizer.swift +8 -0
- package/nitrogen/generated/android/c++/JHybridMyazaTextRecognizerSpec.cpp +21 -0
- package/nitrogen/generated/android/c++/JHybridMyazaTextRecognizerSpec.hpp +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaTextRecognizerSpec.kt +8 -0
- package/nitrogen/generated/ios/c++/HybridMyazaTextRecognizerSpecSwift.hpp +16 -0
- package/nitrogen/generated/ios/swift/HybridMyazaTextRecognizerSpec.swift +2 -0
- package/nitrogen/generated/ios/swift/HybridMyazaTextRecognizerSpec_cxx.swift +31 -0
- package/nitrogen/generated/shared/c++/HybridMyazaTextRecognizerSpec.cpp +2 -0
- package/nitrogen/generated/shared/c++/HybridMyazaTextRecognizerSpec.hpp +2 -0
- package/package.json +24 -14
- package/src/MyazaKYC.tsx +4 -11
- package/src/assets/fonts/Karla_400Regular.ttf +0 -0
- package/src/assets/fonts/Karla_500Medium.ttf +0 -0
- package/src/assets/fonts/Karla_600SemiBold.ttf +0 -0
- package/src/assets/fonts/Karla_700Bold.ttf +0 -0
- package/src/assets/fonts/OFL-Karla.txt +93 -0
- package/src/assets/fonts/OFL-SpaceGrotesk.txt +93 -0
- package/src/assets/fonts/README.md +12 -0
- package/src/assets/fonts/SpaceGrotesk_500Medium.ttf +0 -0
- package/src/assets/fonts/SpaceGrotesk_600SemiBold.ttf +0 -0
- package/src/assets/fonts/SpaceGrotesk_700Bold.ttf +0 -0
- package/src/capture/useAutoCapture.ts +8 -1
- package/src/components/DocumentCropper.tsx +30 -24
- package/src/components/DocumentReview.tsx +11 -1
- package/src/components/DocumentReviewSide.tsx +6 -2
- package/src/components/DocumentReviewZoom.tsx +5 -1
- package/src/components/GlassIconButton.tsx +2 -5
- package/src/components/Icon.tsx +17 -13
- package/src/components/KycFlow.tsx +10 -2
- package/src/components/KycSheet.tsx +0 -1
- package/src/components/MyazaButton.tsx +9 -0
- package/src/components/RequiredDocumentPill.tsx +114 -0
- package/src/components/StepHeader.tsx +23 -3
- package/src/components/StepView.tsx +3 -0
- package/src/components/documentReviewCopy.ts +39 -0
- package/src/components/fonts.ts +14 -17
- package/src/components/icons/glyphs.ts +35 -0
- package/src/components/icons/index.ts +3 -0
- package/src/components/icons/map.ts +165 -0
- package/src/components/icons/names.ts +86 -0
- package/src/components/stepHeaderMeta.tsx +21 -1
- package/src/config/documentCaptureMethods.ts +31 -0
- package/src/config/stepOrder.ts +20 -5
- package/src/config/supportingDocuments.ts +131 -0
- package/src/config/theme.ts +29 -3
- package/src/config/workflowMerge.ts +15 -0
- package/src/index.ts +5 -10
- package/src/lib/documentCaptureCheck.ts +136 -0
- package/src/lib/model-ready.ts +82 -0
- package/src/lib/prime-models.ts +50 -0
- package/src/lib/resubmit.ts +36 -7
- package/src/lib/selfie-sharpness.ts +211 -0
- package/src/lib/supportingDocumentsIntro.ts +35 -0
- package/src/liveness/avatarSource.ts +59 -0
- package/src/liveness/useModelReady.ts +9 -55
- package/src/mrz/extract.ts +71 -4
- package/src/mrz/textRecognizer.ts +36 -0
- package/src/mrz/useTextModelReady.ts +21 -0
- package/src/screens/BusinessDocumentSlot.tsx +37 -8
- package/src/screens/DocumentCaptureStep.tsx +177 -93
- package/src/screens/LivenessAvatar.tsx +18 -13
- package/src/screens/LivenessStep.tsx +37 -0
- package/src/screens/MrzScanView.tsx +33 -0
- package/src/screens/SupportingDocumentCard.tsx +134 -0
- package/src/screens/SupportingDocumentsStep.tsx +203 -0
- package/src/screens/consent/model.ts +22 -5
- package/src/screens/document/CaptureCheckNotice.tsx +77 -0
- package/src/screens/document/UploadPhase.tsx +177 -0
- package/src/screens/document/useDocumentCamera.ts +109 -0
- package/src/screens/liveness/CaptureRing.tsx +31 -17
- package/src/screens/liveness/LivenessOutcome.tsx +4 -1
- package/src/screens/liveness/SelfiePreview.tsx +19 -4
- package/src/screens/supportingDocumentParts.tsx +126 -0
- package/src/screens/useBusinessDocumentAttach.ts +20 -9
- package/src/services/api-types.ts +31 -2
- package/src/services/api.ts +19 -3
- package/src/services/deviceMetadata.ts +1 -1
- package/src/services/mediaCompress.ts +23 -5
- package/src/specs/MyazaTextRecognizer.nitro.ts +29 -0
- package/src/store/derive.ts +11 -0
- package/src/store/kycStore.ts +23 -2
- package/src/store/session.ts +18 -0
- package/src/store/state.ts +26 -0
- package/src/store/submit.ts +11 -0
- package/src/types/config.ts +25 -0
- package/src/types/workflow.ts +39 -0
- package/src/MyazaBiometricAuth.tsx +0 -119
- package/src/assets/liveness/Blink.gif +0 -0
- package/src/assets/liveness/Nod.gif +0 -0
- package/src/assets/liveness/Smile.gif +0 -0
- package/src/assets/liveness/Turn.gif +0 -0
- package/src/components/icon-map.ts +0 -176
- package/src/lib/biometric-auth.ts +0 -52
- package/src/screens/biometric/BiometricAuthFlow.tsx +0 -170
- package/src/services/api-biometric.ts +0 -37
- package/src/services/api-types-biometric.ts +0 -41
package/src/components/fonts.ts
CHANGED
|
@@ -1,26 +1,29 @@
|
|
|
1
1
|
import type { TextStyle } from 'react-native';
|
|
2
2
|
import { useFonts } from 'expo-font';
|
|
3
|
-
import { SpaceGrotesk_500Medium, SpaceGrotesk_600SemiBold, SpaceGrotesk_700Bold } from '@expo-google-fonts/space-grotesk';
|
|
4
|
-
import { Karla_400Regular, Karla_500Medium, Karla_600SemiBold, Karla_700Bold } from '@expo-google-fonts/karla';
|
|
5
3
|
import { fontFamilyFor } from '../config/font-resolve';
|
|
6
4
|
import { useTheme } from './theme-provider';
|
|
7
5
|
|
|
8
6
|
export { fontFamilyFor, markFamilyName, brandFamilyName, BRAND_WEIGHTS } from '../config/font-resolve';
|
|
9
7
|
|
|
10
|
-
|
|
11
8
|
// Typography fonts — the SAME families the Flutter SDK uses via google_fonts:
|
|
12
9
|
// Space Grotesk for headings, Karla for body. Loaded at runtime through
|
|
13
10
|
// expo-font (already natively linked), so no native rebuild is needed. Each
|
|
14
11
|
// weight is its own RN font family, so we resolve family-by-weight below.
|
|
15
|
-
|
|
12
|
+
//
|
|
13
|
+
// The files are VENDORED under src/assets/fonts and required one by one (see
|
|
14
|
+
// the README there). They used to be named imports from the
|
|
15
|
+
// @expo-google-fonts packages, whose index requires EVERY weight the family
|
|
16
|
+
// ships — and Metro bundles every `require` it can see, used or not — so 19
|
|
17
|
+
// font files reached every integrator's app for the seven faces the SDK draws
|
|
18
|
+
// with. Measured at 1.85 MB on a real integrator's release APK.
|
|
16
19
|
export const MYAZA_FONTS = {
|
|
17
|
-
SpaceGrotesk_500Medium,
|
|
18
|
-
SpaceGrotesk_600SemiBold,
|
|
19
|
-
SpaceGrotesk_700Bold,
|
|
20
|
-
Karla_400Regular,
|
|
21
|
-
Karla_500Medium,
|
|
22
|
-
Karla_600SemiBold,
|
|
23
|
-
Karla_700Bold,
|
|
20
|
+
SpaceGrotesk_500Medium: require('../assets/fonts/SpaceGrotesk_500Medium.ttf'),
|
|
21
|
+
SpaceGrotesk_600SemiBold: require('../assets/fonts/SpaceGrotesk_600SemiBold.ttf'),
|
|
22
|
+
SpaceGrotesk_700Bold: require('../assets/fonts/SpaceGrotesk_700Bold.ttf'),
|
|
23
|
+
Karla_400Regular: require('../assets/fonts/Karla_400Regular.ttf'),
|
|
24
|
+
Karla_500Medium: require('../assets/fonts/Karla_500Medium.ttf'),
|
|
25
|
+
Karla_600SemiBold: require('../assets/fonts/Karla_600SemiBold.ttf'),
|
|
26
|
+
Karla_700Bold: require('../assets/fonts/Karla_700Bold.ttf'),
|
|
24
27
|
};
|
|
25
28
|
|
|
26
29
|
/** Loads the Myaza fonts. Returns true once they're ready (system font until then). */
|
|
@@ -29,12 +32,6 @@ export function useMyazaFonts(): boolean {
|
|
|
29
32
|
return loaded;
|
|
30
33
|
}
|
|
31
34
|
|
|
32
|
-
/**
|
|
33
|
-
* Resolves the concrete RN font family for a (heading vs body, weight) pair.
|
|
34
|
-
* Returns `undefined` until fonts are loaded so text falls back to the system
|
|
35
|
-
* font + `fontWeight` without triggering an "unrecognized font" warning.
|
|
36
|
-
*/
|
|
37
|
-
|
|
38
35
|
/**
|
|
39
36
|
* The body font family for a TEXT INPUT, at the given weight.
|
|
40
37
|
*
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { IconSvgElement } from '@hugeicons/react-native';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Glyphs Hugeicons does not carry, drawn in the set's own stroke language.
|
|
5
|
+
*
|
|
6
|
+
* Kept to the strict minimum: every other icon comes from the set, and this
|
|
7
|
+
* file exists only where the set has no equivalent at all. It MIRRORS the web
|
|
8
|
+
* SDK's `components/icons/glyphs.ts` - same geometry, same stroke attributes -
|
|
9
|
+
* so the two platforms draw one arrow rather than two near-misses.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const STROKE = {
|
|
13
|
+
stroke: 'currentColor',
|
|
14
|
+
strokeLinecap: 'round',
|
|
15
|
+
strokeLinejoin: 'round',
|
|
16
|
+
strokeWidth: '1.5',
|
|
17
|
+
} as const;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A LONG left arrow - a full-width shaft with a small head.
|
|
21
|
+
*
|
|
22
|
+
* Hugeicons has no such glyph. Its longest one-way left arrow is
|
|
23
|
+
* `ArrowLeft02Icon`, whose shaft spans 13.5 of the 24-unit viewBox; the rest
|
|
24
|
+
* are shorter, bare chevrons, or arrows into a vertical bar. This SDK's back
|
|
25
|
+
* control drew Lucide's `MoveLeft` before the migration, whose shaft is 20
|
|
26
|
+
* units, so moving to the set would visibly shorten it.
|
|
27
|
+
*
|
|
28
|
+
* The geometry is Lucide's, inset to sit on the same optical margins as its
|
|
29
|
+
* Hugeicons siblings (they start at ~3.5 rather than Lucide's 2), and the
|
|
30
|
+
* stroke attributes are copied from the set so weight and joins match exactly.
|
|
31
|
+
*/
|
|
32
|
+
export const LONG_ARROW_LEFT: IconSvgElement = [
|
|
33
|
+
['path', { d: 'M20.5 12H3.5', ...STROKE, key: '0' }],
|
|
34
|
+
['path', { d: 'M8 7.5L3.5 12L8 16.5', ...STROKE, key: '1' }],
|
|
35
|
+
];
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import type { IconSvgElement } from '@hugeicons/react-native';
|
|
2
|
+
|
|
3
|
+
import Add01Icon from '@hugeicons/core-free-icons/Add01Icon';
|
|
4
|
+
import AlertCircleIcon from '@hugeicons/core-free-icons/AlertCircleIcon';
|
|
5
|
+
import BadgeCheckIcon from '@hugeicons/core-free-icons/BadgeCheckIcon';
|
|
6
|
+
import BellRingIcon from '@hugeicons/core-free-icons/BellRingIcon';
|
|
7
|
+
import BookUserIcon from '@hugeicons/core-free-icons/BookUserIcon';
|
|
8
|
+
import BubbleChatIcon from '@hugeicons/core-free-icons/BubbleChatIcon';
|
|
9
|
+
import Building02Icon from '@hugeicons/core-free-icons/Building02Icon';
|
|
10
|
+
import BulbIcon from '@hugeicons/core-free-icons/BulbIcon';
|
|
11
|
+
import Calendar01Icon from '@hugeicons/core-free-icons/Calendar01Icon';
|
|
12
|
+
import Camera01Icon from '@hugeicons/core-free-icons/Camera01Icon';
|
|
13
|
+
import CameraOff01Icon from '@hugeicons/core-free-icons/CameraOff01Icon';
|
|
14
|
+
import Cancel01Icon from '@hugeicons/core-free-icons/Cancel01Icon';
|
|
15
|
+
import CancelCircleIcon from '@hugeicons/core-free-icons/CancelCircleIcon';
|
|
16
|
+
import Car01Icon from '@hugeicons/core-free-icons/Car01Icon';
|
|
17
|
+
import CheckIcon from '@hugeicons/core-free-icons/CheckIcon';
|
|
18
|
+
import CheckmarkCircle02Icon from '@hugeicons/core-free-icons/CheckmarkCircle02Icon';
|
|
19
|
+
import ChevronDownIcon from '@hugeicons/core-free-icons/ChevronDownIcon';
|
|
20
|
+
import ChevronLeftIcon from '@hugeicons/core-free-icons/ChevronLeftIcon';
|
|
21
|
+
import ChevronRightIcon from '@hugeicons/core-free-icons/ChevronRightIcon';
|
|
22
|
+
import CircleDashedIcon from '@hugeicons/core-free-icons/CircleDashedIcon';
|
|
23
|
+
import ContactIcon from '@hugeicons/core-free-icons/ContactIcon';
|
|
24
|
+
import CopyIcon from '@hugeicons/core-free-icons/CopyIcon';
|
|
25
|
+
import CreditCardIcon from '@hugeicons/core-free-icons/CreditCardIcon';
|
|
26
|
+
import ExpandIcon from '@hugeicons/core-free-icons/ExpandIcon';
|
|
27
|
+
import FaceIdIcon from '@hugeicons/core-free-icons/FaceIdIcon';
|
|
28
|
+
import File02Icon from '@hugeicons/core-free-icons/File02Icon';
|
|
29
|
+
import FilterIcon from '@hugeicons/core-free-icons/FilterIcon';
|
|
30
|
+
import FingerPrintIcon from '@hugeicons/core-free-icons/FingerPrintIcon';
|
|
31
|
+
import FlashOffIcon from '@hugeicons/core-free-icons/FlashOffIcon';
|
|
32
|
+
import FlaskConicalIcon from '@hugeicons/core-free-icons/FlaskConicalIcon';
|
|
33
|
+
import GlobeIcon from '@hugeicons/core-free-icons/GlobeIcon';
|
|
34
|
+
import Gps02Icon from '@hugeicons/core-free-icons/Gps02Icon';
|
|
35
|
+
import HelpCircleIcon from '@hugeicons/core-free-icons/HelpCircleIcon';
|
|
36
|
+
import Home01Icon from '@hugeicons/core-free-icons/Home01Icon';
|
|
37
|
+
import IdentityCardIcon from '@hugeicons/core-free-icons/IdentityCardIcon';
|
|
38
|
+
import Image01Icon from '@hugeicons/core-free-icons/Image01Icon';
|
|
39
|
+
import InformationCircleIcon from '@hugeicons/core-free-icons/InformationCircleIcon';
|
|
40
|
+
import LandmarkIcon from '@hugeicons/core-free-icons/LandmarkIcon';
|
|
41
|
+
import Link02Icon from '@hugeicons/core-free-icons/Link02Icon';
|
|
42
|
+
import Location02Icon from '@hugeicons/core-free-icons/Location02Icon';
|
|
43
|
+
import LocationCheck01Icon from '@hugeicons/core-free-icons/LocationCheck01Icon';
|
|
44
|
+
import LockIcon from '@hugeicons/core-free-icons/LockIcon';
|
|
45
|
+
import Mail01Icon from '@hugeicons/core-free-icons/Mail01Icon';
|
|
46
|
+
import MapPinHouseIcon from '@hugeicons/core-free-icons/MapPinHouseIcon';
|
|
47
|
+
import MapPinIcon from '@hugeicons/core-free-icons/MapPinIcon';
|
|
48
|
+
import Message01Icon from '@hugeicons/core-free-icons/Message01Icon';
|
|
49
|
+
import MinusSignIcon from '@hugeicons/core-free-icons/MinusSignIcon';
|
|
50
|
+
import MoonIcon from '@hugeicons/core-free-icons/MoonIcon';
|
|
51
|
+
import NfcIcon from '@hugeicons/core-free-icons/NfcIcon';
|
|
52
|
+
import PencilEdit01Icon from '@hugeicons/core-free-icons/PencilEdit01Icon';
|
|
53
|
+
import PencilIcon from '@hugeicons/core-free-icons/PencilIcon';
|
|
54
|
+
import Radar01Icon from '@hugeicons/core-free-icons/Radar01Icon';
|
|
55
|
+
import RefreshCwIcon from '@hugeicons/core-free-icons/RefreshCwIcon';
|
|
56
|
+
import RefreshIcon from '@hugeicons/core-free-icons/RefreshIcon';
|
|
57
|
+
import ScanIcon from '@hugeicons/core-free-icons/ScanIcon';
|
|
58
|
+
import Search01Icon from '@hugeicons/core-free-icons/Search01Icon';
|
|
59
|
+
import SecurityCheckIcon from '@hugeicons/core-free-icons/SecurityCheckIcon';
|
|
60
|
+
import Share01Icon from '@hugeicons/core-free-icons/Share01Icon';
|
|
61
|
+
import SlidersHorizontalIcon from '@hugeicons/core-free-icons/SlidersHorizontalIcon';
|
|
62
|
+
import SmartPhone01Icon from '@hugeicons/core-free-icons/SmartPhone01Icon';
|
|
63
|
+
import StampIcon from '@hugeicons/core-free-icons/StampIcon';
|
|
64
|
+
import Sun01Icon from '@hugeicons/core-free-icons/Sun01Icon';
|
|
65
|
+
import Timer01Icon from '@hugeicons/core-free-icons/Timer01Icon';
|
|
66
|
+
import Upload01Icon from '@hugeicons/core-free-icons/Upload01Icon';
|
|
67
|
+
import UserAdd01Icon from '@hugeicons/core-free-icons/UserAdd01Icon';
|
|
68
|
+
import UserCircleIcon from '@hugeicons/core-free-icons/UserCircleIcon';
|
|
69
|
+
import UserGroupIcon from '@hugeicons/core-free-icons/UserGroupIcon';
|
|
70
|
+
import VideoOffIcon from '@hugeicons/core-free-icons/VideoOffIcon';
|
|
71
|
+
import VoteIcon from '@hugeicons/core-free-icons/VoteIcon';
|
|
72
|
+
import ZapIcon from '@hugeicons/core-free-icons/ZapIcon';
|
|
73
|
+
|
|
74
|
+
import { LONG_ARROW_LEFT } from './glyphs';
|
|
75
|
+
import type { IconName } from './names';
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Each role, and the glyph that draws it.
|
|
79
|
+
*
|
|
80
|
+
* Imports are DEEP - one module per icon - and must stay that way. Importing
|
|
81
|
+
* from the package index instead pulls the whole 6,025-icon pack into the
|
|
82
|
+
* bundle: measured at +8.33 MB for five icons against +7 KB for the same five
|
|
83
|
+
* imported this way. The pack declares a `./*` export for exactly this, and
|
|
84
|
+
* iconImports.test.ts fails the build on a barrel import.
|
|
85
|
+
*
|
|
86
|
+
* The glyphs are the ones the web SDK and Flutter already chose for these
|
|
87
|
+
* names, so the three SDKs draw one set. Where neither had a choice to copy,
|
|
88
|
+
* it was made against the catalogue by meaning and is noted below.
|
|
89
|
+
*/
|
|
90
|
+
export const ICONS: Record<IconName, IconSvgElement> = {
|
|
91
|
+
close: Cancel01Icon,
|
|
92
|
+
back: LONG_ARROW_LEFT,
|
|
93
|
+
moon: MoonIcon,
|
|
94
|
+
sun: Sun01Icon,
|
|
95
|
+
check: CheckIcon,
|
|
96
|
+
lock: LockIcon,
|
|
97
|
+
alert: AlertCircleIcon,
|
|
98
|
+
// The environment banner's mark, shared with the web SDK (same lucide glyph)
|
|
99
|
+
// and mirrored by Flutter's Icons.science_outlined.
|
|
100
|
+
flask: FlaskConicalIcon,
|
|
101
|
+
refresh: RefreshCwIcon,
|
|
102
|
+
'refresh-ccw': RefreshIcon,
|
|
103
|
+
maximize: ExpandIcon,
|
|
104
|
+
calendar: Calendar01Icon,
|
|
105
|
+
image: Image01Icon,
|
|
106
|
+
'chevron-left': ChevronLeftIcon,
|
|
107
|
+
'chevron-right': ChevronRightIcon,
|
|
108
|
+
'chevron-down': ChevronDownIcon,
|
|
109
|
+
shield: SecurityCheckIcon,
|
|
110
|
+
'badge-check': BadgeCheckIcon,
|
|
111
|
+
'circle-dashed': CircleDashedIcon,
|
|
112
|
+
'circle-check': CheckmarkCircle02Icon,
|
|
113
|
+
'circle-help': HelpCircleIcon,
|
|
114
|
+
'circle-x': CancelCircleIcon,
|
|
115
|
+
'building-2': Building02Icon,
|
|
116
|
+
stamp: StampIcon,
|
|
117
|
+
user: UserCircleIcon,
|
|
118
|
+
info: InformationCircleIcon,
|
|
119
|
+
'user-plus': UserAdd01Icon,
|
|
120
|
+
plus: Add01Icon,
|
|
121
|
+
link: Link02Icon,
|
|
122
|
+
copy: CopyIcon,
|
|
123
|
+
share: Share01Icon,
|
|
124
|
+
users: UserGroupIcon,
|
|
125
|
+
'scan-line': ScanIcon,
|
|
126
|
+
'scan-face': FaceIdIcon,
|
|
127
|
+
nfc: NfcIcon,
|
|
128
|
+
filter: FilterIcon,
|
|
129
|
+
pencil: PencilIcon,
|
|
130
|
+
'pencil-line': PencilEdit01Icon,
|
|
131
|
+
search: Search01Icon,
|
|
132
|
+
zap: ZapIcon,
|
|
133
|
+
'zap-off': FlashOffIcon,
|
|
134
|
+
timer: Timer01Icon,
|
|
135
|
+
fingerprint: FingerPrintIcon,
|
|
136
|
+
'credit-card': CreditCardIcon,
|
|
137
|
+
globe: GlobeIcon,
|
|
138
|
+
car: Car01Icon,
|
|
139
|
+
vote: VoteIcon,
|
|
140
|
+
'file-text': File02Icon,
|
|
141
|
+
house: Home01Icon,
|
|
142
|
+
landmark: LandmarkIcon,
|
|
143
|
+
'map-pin': MapPinIcon,
|
|
144
|
+
'map-pin-house': MapPinHouseIcon,
|
|
145
|
+
'map-pin-check': LocationCheck01Icon,
|
|
146
|
+
'map-pinned': Location02Icon,
|
|
147
|
+
radar: Radar01Icon,
|
|
148
|
+
'bell-ring': BellRingIcon,
|
|
149
|
+
sliders: SlidersHorizontalIcon,
|
|
150
|
+
locate: Gps02Icon,
|
|
151
|
+
minus: MinusSignIcon,
|
|
152
|
+
'id-card': IdentityCardIcon,
|
|
153
|
+
contact: ContactIcon,
|
|
154
|
+
passport: BookUserIcon,
|
|
155
|
+
camera: Camera01Icon,
|
|
156
|
+
'camera-off': CameraOff01Icon,
|
|
157
|
+
'video-off': VideoOffIcon,
|
|
158
|
+
upload: Upload01Icon,
|
|
159
|
+
x: Cancel01Icon,
|
|
160
|
+
lightbulb: BulbIcon,
|
|
161
|
+
mail: Mail01Icon,
|
|
162
|
+
smartphone: SmartPhone01Icon,
|
|
163
|
+
'message-square': Message01Icon,
|
|
164
|
+
'message-circle': BubbleChatIcon,
|
|
165
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The icon roles this SDK draws, by MEANING rather than by glyph name.
|
|
3
|
+
*
|
|
4
|
+
* This is the boundary that lets the set underneath change without touching a
|
|
5
|
+
* call site: a screen asks for 'close', and `icons/map.ts` decides what that
|
|
6
|
+
* looks like. Adding a role here means adding its glyph there.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type IconName =
|
|
10
|
+
// chrome
|
|
11
|
+
| 'close'
|
|
12
|
+
| 'back'
|
|
13
|
+
| 'moon'
|
|
14
|
+
| 'sun'
|
|
15
|
+
| 'check'
|
|
16
|
+
| 'lock'
|
|
17
|
+
| 'alert'
|
|
18
|
+
| 'flask'
|
|
19
|
+
| 'refresh'
|
|
20
|
+
| 'refresh-ccw'
|
|
21
|
+
| 'maximize'
|
|
22
|
+
| 'calendar'
|
|
23
|
+
| 'image'
|
|
24
|
+
| 'chevron-left'
|
|
25
|
+
| 'chevron-right'
|
|
26
|
+
| 'chevron-down'
|
|
27
|
+
// consent hero + process steps
|
|
28
|
+
| 'shield'
|
|
29
|
+
| 'badge-check'
|
|
30
|
+
| 'circle-dashed'
|
|
31
|
+
| 'circle-check'
|
|
32
|
+
| 'circle-help'
|
|
33
|
+
| 'circle-x'
|
|
34
|
+
| 'building-2'
|
|
35
|
+
| 'stamp'
|
|
36
|
+
| 'user'
|
|
37
|
+
| 'user-plus'
|
|
38
|
+
| 'plus'
|
|
39
|
+
| 'info'
|
|
40
|
+
| 'users'
|
|
41
|
+
| 'scan-line'
|
|
42
|
+
| 'scan-face'
|
|
43
|
+
| 'nfc'
|
|
44
|
+
| 'filter'
|
|
45
|
+
| 'pencil'
|
|
46
|
+
| 'pencil-line'
|
|
47
|
+
| 'search'
|
|
48
|
+
| 'zap'
|
|
49
|
+
| 'link'
|
|
50
|
+
| 'copy'
|
|
51
|
+
| 'share'
|
|
52
|
+
| 'zap-off'
|
|
53
|
+
| 'timer'
|
|
54
|
+
// id-type glyphs
|
|
55
|
+
| 'fingerprint'
|
|
56
|
+
| 'credit-card'
|
|
57
|
+
| 'globe'
|
|
58
|
+
| 'car'
|
|
59
|
+
| 'vote'
|
|
60
|
+
| 'file-text'
|
|
61
|
+
| 'house'
|
|
62
|
+
| 'landmark'
|
|
63
|
+
| 'id-card'
|
|
64
|
+
| 'contact'
|
|
65
|
+
| 'passport'
|
|
66
|
+
| 'camera'
|
|
67
|
+
| 'camera-off'
|
|
68
|
+
| 'video-off'
|
|
69
|
+
| 'upload'
|
|
70
|
+
| 'x'
|
|
71
|
+
// address collection (map picker + the presence milestone track)
|
|
72
|
+
| 'map-pin'
|
|
73
|
+
| 'map-pin-house'
|
|
74
|
+
| 'map-pin-check'
|
|
75
|
+
| 'map-pinned'
|
|
76
|
+
| 'radar'
|
|
77
|
+
| 'bell-ring'
|
|
78
|
+
| 'sliders'
|
|
79
|
+
| 'locate'
|
|
80
|
+
| 'minus'
|
|
81
|
+
| 'lightbulb'
|
|
82
|
+
// contact verification (OTP channels + step footer)
|
|
83
|
+
| 'mail'
|
|
84
|
+
| 'smartphone'
|
|
85
|
+
| 'message-square'
|
|
86
|
+
| 'message-circle';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ID_TYPES } from '../config/idTypes';
|
|
2
2
|
import { poaDocumentTypes, poaMaxAgeDays, poaNamePolicy } from '../config/proofOfAddress';
|
|
3
3
|
import { documentCaptureMeta } from '../screens/DocumentCaptureStep';
|
|
4
|
+
import { documentCaptureMethods } from '../config/documentCaptureMethods';
|
|
4
5
|
import { questionnaireMeta } from '../screens/QuestionnaireStep';
|
|
5
6
|
import { contactCodeLength, contactMeta, type ContactChallenge } from '../config/contact';
|
|
6
7
|
|
|
@@ -9,6 +10,8 @@ import { addressStepMeta } from '../screens/address';
|
|
|
9
10
|
import { countrySelectMeta } from '../screens/CountrySelectStep';
|
|
10
11
|
import { businessDetailsMeta } from '../screens/BusinessDetailsStep';
|
|
11
12
|
import { businessKeyPeopleMeta } from '../screens/BusinessKeyPeopleStep';
|
|
13
|
+
import { supportingDocumentsMeta } from '../screens/SupportingDocumentsStep';
|
|
14
|
+
import { resolveSupportingDocuments, verifiedIdsFor } from '../config/supportingDocuments';
|
|
12
15
|
import { businessDocumentsMeta } from '../screens/BusinessDocumentsStep';
|
|
13
16
|
import { applicantRoleMeta } from '../screens/ApplicantRoleStep';
|
|
14
17
|
import { nfcMeta } from '../screens/NfcStep';
|
|
@@ -48,6 +51,9 @@ export interface StepHeaderContext {
|
|
|
48
51
|
/** The proof-of-address kind the applicant has picked, so the header can ask
|
|
49
52
|
* for what the workflow's name rule wants on THAT document. */
|
|
50
53
|
poaDocumentType?: string | null;
|
|
54
|
+
/** The committed multi-ID slots, so the supporting-documents header resolves
|
|
55
|
+
* the SAME list the screen shows. */
|
|
56
|
+
multiIdSlots?: Array<{ idType: string }>;
|
|
51
57
|
}
|
|
52
58
|
|
|
53
59
|
export function stepHeaderMeta(
|
|
@@ -61,6 +67,7 @@ export function stepHeaderMeta(
|
|
|
61
67
|
addressIntroPending,
|
|
62
68
|
addressEntranceFraming,
|
|
63
69
|
poaDocumentType,
|
|
70
|
+
multiIdSlots,
|
|
64
71
|
}: StepHeaderContext,
|
|
65
72
|
): { title: string; description: string | null } {
|
|
66
73
|
const label = labelFor(country, selectedIdType);
|
|
@@ -77,7 +84,11 @@ export function stepHeaderMeta(
|
|
|
77
84
|
case 'document-capture':
|
|
78
85
|
// Phase-aware title/description live in the header (synced from the
|
|
79
86
|
// capture screen via `documentCapturePhase`) — mirrors Flutter.
|
|
80
|
-
return documentCaptureMeta(
|
|
87
|
+
return documentCaptureMeta(
|
|
88
|
+
documentCapturePhase,
|
|
89
|
+
label,
|
|
90
|
+
documentCaptureMethods(config).scan ? 'scan' : 'upload',
|
|
91
|
+
);
|
|
81
92
|
case 'liveness':
|
|
82
93
|
return { title: 'Face Verification', description: 'Follow the on-screen instructions' };
|
|
83
94
|
case 'questionnaire':
|
|
@@ -116,6 +127,15 @@ export function stepHeaderMeta(
|
|
|
116
127
|
return businessKeyPeopleMeta;
|
|
117
128
|
case 'business-documents':
|
|
118
129
|
return businessDocumentsMeta;
|
|
130
|
+
case 'supporting-documents':
|
|
131
|
+
// Resolved exactly as the screen resolves its own slots, or the header
|
|
132
|
+
// could name a number the body does not show.
|
|
133
|
+
return supportingDocumentsMeta(
|
|
134
|
+
resolveSupportingDocuments(
|
|
135
|
+
config.supportingDocuments,
|
|
136
|
+
verifiedIdsFor({ country, idType: selectedIdType, multiIdSlots }),
|
|
137
|
+
),
|
|
138
|
+
);
|
|
119
139
|
case 'applicant-role':
|
|
120
140
|
return applicantRoleMeta;
|
|
121
141
|
case 'nfc':
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// How the applicant may put a document in front of us.
|
|
3
|
+
//
|
|
4
|
+
// Two workflow switches, each on unless set to `false`: `allowDocumentScan`
|
|
5
|
+
// (photograph it with the live camera) and `allowDocumentUpload` (choose a
|
|
6
|
+
// photo of it from the device). The server refuses to publish a workflow with
|
|
7
|
+
// both off, but a stored or prop-supplied config can still carry that, and a
|
|
8
|
+
// document step with no way in is a dead end. So the camera comes back on in
|
|
9
|
+
// that case: it is the method the step was built around.
|
|
10
|
+
//
|
|
11
|
+
// Screens read the pair through here and never the raw keys, so the fallback
|
|
12
|
+
// is one rule rather than a check every screen has to remember. Mirrors the
|
|
13
|
+
// web and Flutter SDKs.
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
export interface DocumentCaptureMethods {
|
|
17
|
+
/** Photograph the document with the live camera. */
|
|
18
|
+
scan: boolean;
|
|
19
|
+
/** Choose a photo of the document from the device. */
|
|
20
|
+
upload: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function documentCaptureMethods(config: {
|
|
24
|
+
allowDocumentScan?: boolean;
|
|
25
|
+
allowDocumentUpload?: boolean;
|
|
26
|
+
}): DocumentCaptureMethods {
|
|
27
|
+
const upload = config.allowDocumentUpload !== false;
|
|
28
|
+
// With upload off the camera is the only way left, whatever its flag says.
|
|
29
|
+
const scan = upload ? config.allowDocumentScan !== false : true;
|
|
30
|
+
return { scan, upload };
|
|
31
|
+
}
|
package/src/config/stepOrder.ts
CHANGED
|
@@ -44,6 +44,14 @@ export interface StepOrderOptions {
|
|
|
44
44
|
hasEmailVerification: boolean;
|
|
45
45
|
hasPhoneVerification: boolean;
|
|
46
46
|
hasPoa: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Whether the supporting-documents step has anything to ask for on THIS
|
|
49
|
+
* attempt. Resolved by the caller (config/supportingDocuments.ts) because
|
|
50
|
+
* the list depends on the ID the person actually picked — a document scoped
|
|
51
|
+
* to one ID is not asked of somebody who used another, and a step with an
|
|
52
|
+
* empty list must not appear at all.
|
|
53
|
+
*/
|
|
54
|
+
hasSupportingDocuments: boolean;
|
|
47
55
|
hasAddressCollection: boolean;
|
|
48
56
|
/**
|
|
49
57
|
* Which address screens the flow has, from the ONE `addressFlowFor` call.
|
|
@@ -120,11 +128,15 @@ function fullStepOrder(o: StepOrderOptions): KYCStep[] {
|
|
|
120
128
|
if (o.scope === 'address') {
|
|
121
129
|
const steps: KYCStep[] = [...openingSteps(o), ...contactSteps(o)];
|
|
122
130
|
if (o.hasPoa) steps.push('proof-of-address');
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
131
|
+
// Gated like the full flow, and like Flutter has always done it: an
|
|
132
|
+
// address flow that asks only for proof of address never opens a map.
|
|
133
|
+
if (o.hasAddressCollection) {
|
|
134
|
+
steps.push(
|
|
135
|
+
...addressFlowSteps(
|
|
136
|
+
o.addressFlow ?? { searchAvailable: false, photoMode: 'optional', streetViewOffered: false },
|
|
137
|
+
),
|
|
138
|
+
);
|
|
139
|
+
}
|
|
128
140
|
if (o.hasQuestionnaire) steps.push('questionnaire');
|
|
129
141
|
steps.push('submitted');
|
|
130
142
|
return steps;
|
|
@@ -143,6 +155,9 @@ function fullStepOrder(o: StepOrderOptions): KYCStep[] {
|
|
|
143
155
|
}
|
|
144
156
|
|
|
145
157
|
const middle: KYCStep[] = [...captureLeg(o)];
|
|
158
|
+
// Paperwork the org files comes BEFORE the address evidence the
|
|
159
|
+
// verification is judged on (user decision 2026-09-22).
|
|
160
|
+
if (o.hasSupportingDocuments) middle.push('supporting-documents');
|
|
146
161
|
if (o.hasPoa) middle.push('proof-of-address');
|
|
147
162
|
// The address flow is FOUR real steps on an individual flow (find it, confirm
|
|
148
163
|
// it, show it, commit it), so the progress bar advances through them and back
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { SupportingDocumentsConfig } from '../types/workflow';
|
|
2
|
+
|
|
3
|
+
// Which supporting documents this flow asks for, given the ID the person
|
|
4
|
+
// actually verified with.
|
|
5
|
+
//
|
|
6
|
+
// MIRROR of the server's `requestedSupportingDocuments`
|
|
7
|
+
// (kyc-core src/lib/workflows/supporting-documents-config.ts) and of the web
|
|
8
|
+
// SDK's lib/supporting-documents.ts. Three copies of one rule: the mobile SDKs
|
|
9
|
+
// cannot import the web package, so a change here changes all three in the
|
|
10
|
+
// same commit. The server VALIDATES what the client produced, so a client that
|
|
11
|
+
// resolved differently just builds submissions the server refuses.
|
|
12
|
+
//
|
|
13
|
+
// There is no catalogue to mirror: a document is whatever the ORG named it, so
|
|
14
|
+
// the SDK renders the title and guidance the workflow sent rather than
|
|
15
|
+
// captioning a key it recognises.
|
|
16
|
+
|
|
17
|
+
export interface RequestedSupportingDocument {
|
|
18
|
+
key: string;
|
|
19
|
+
label: string;
|
|
20
|
+
/** Guidance under the slot, when the author wrote some. */
|
|
21
|
+
description: string | null;
|
|
22
|
+
required: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* The names of the values the server will read off it, for the card to show.
|
|
25
|
+
*
|
|
26
|
+
* DISPLAY ONLY, and deliberately not a mirror of the server's own field
|
|
27
|
+
* resolution: it drops blanks and repeats and stops there. The server decides
|
|
28
|
+
* what is actually read, and an extra name on a chip costs an applicant
|
|
29
|
+
* nothing.
|
|
30
|
+
*/
|
|
31
|
+
reads: string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The names on one document's fields: what the applicant is told we read. */
|
|
35
|
+
function documentReads(fields: Array<{ label?: string }> | undefined): string[] {
|
|
36
|
+
const seen = new Set<string>();
|
|
37
|
+
const out: string[] = [];
|
|
38
|
+
for (const field of fields ?? []) {
|
|
39
|
+
const label = field.label?.trim();
|
|
40
|
+
if (!label || seen.has(label.toLowerCase())) continue;
|
|
41
|
+
seen.add(label.toLowerCase());
|
|
42
|
+
out.push(label);
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** `${country}/${idType}` — the composite a document's `idTypes` is written in. */
|
|
48
|
+
export function idComposite(country: string, idType: string): string {
|
|
49
|
+
return `${country.trim().toUpperCase()}/${idType.trim()}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The documents to ask for. An empty result means the step does not appear:
|
|
54
|
+
* a document that exists only because the person used a particular ID must not
|
|
55
|
+
* be put in front of somebody who used another.
|
|
56
|
+
*/
|
|
57
|
+
export function resolveSupportingDocuments(
|
|
58
|
+
config: SupportingDocumentsConfig | undefined | null,
|
|
59
|
+
verifiedIds: string[],
|
|
60
|
+
): RequestedSupportingDocument[] {
|
|
61
|
+
if (!config?.enabled) return [];
|
|
62
|
+
const wanted = new Set(verifiedIds.map((id) => id.toUpperCase()));
|
|
63
|
+
const seen = new Set<string>();
|
|
64
|
+
const out: RequestedSupportingDocument[] = [];
|
|
65
|
+
for (const entry of config.types ?? []) {
|
|
66
|
+
const label = entry.label?.trim();
|
|
67
|
+
// A nameless slot reaches nobody. Publish refuses one, so this only ever
|
|
68
|
+
// bites a draft mid-edit.
|
|
69
|
+
if (!label) continue;
|
|
70
|
+
const scoped = entry.idTypes && entry.idTypes.length > 0;
|
|
71
|
+
const inScope =
|
|
72
|
+
!scoped || entry.idTypes!.some((id) => wanted.has(id.trim().toUpperCase()));
|
|
73
|
+
// `alwaysAsk` keeps the slot on screen for everybody, so the scope decides
|
|
74
|
+
// only who must fill it: an out-of-scope applicant may hand the document
|
|
75
|
+
// over and is never blocked for not having one.
|
|
76
|
+
if (!inScope && entry.alwaysAsk !== true) continue;
|
|
77
|
+
if (seen.has(entry.key)) continue;
|
|
78
|
+
seen.add(entry.key);
|
|
79
|
+
out.push({
|
|
80
|
+
key: entry.key,
|
|
81
|
+
label,
|
|
82
|
+
description: entry.description?.trim() || null,
|
|
83
|
+
required: entry.required === true && inScope,
|
|
84
|
+
reads: documentReads(entry.fields),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The composites this attempt has committed — one per ID, multi-ID included. */
|
|
91
|
+
export function verifiedIdsFor(input: {
|
|
92
|
+
country?: string | null;
|
|
93
|
+
idType?: string | null;
|
|
94
|
+
multiIdSlots?: Array<{ idType: string }>;
|
|
95
|
+
}): string[] {
|
|
96
|
+
if (!input.country) return [];
|
|
97
|
+
const slots = input.multiIdSlots ?? [];
|
|
98
|
+
const ids = slots.length > 0 ? slots.map((s) => s.idType) : input.idType ? [input.idType] : [];
|
|
99
|
+
return [...new Set(ids.map((id) => idComposite(input.country!, id)))];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Whether the flow may ask for a supporting document AT ALL.
|
|
104
|
+
*
|
|
105
|
+
* The CONSENT screen's question, deliberately not the step order's.
|
|
106
|
+
* `hasSupportingDocumentsStep` resolves against the VERIFIED IDS, and consent
|
|
107
|
+
* runs before an ID is picked — so every SCOPED document answers "nothing to
|
|
108
|
+
* ask for" there, and a slip scoped to one ID (the commonest case there is)
|
|
109
|
+
* would go undisclosed.
|
|
110
|
+
*
|
|
111
|
+
* Consent is a DISCLOSURE of what MAY be collected: an applicant seeing a
|
|
112
|
+
* bullet for paperwork they are never asked for is the cheap error, and being
|
|
113
|
+
* asked for undisclosed paperwork is the real one.
|
|
114
|
+
*
|
|
115
|
+
* Still not a raw field check — a disabled step and a nameless entry each
|
|
116
|
+
* promise nothing, the same two gates the resolver applies.
|
|
117
|
+
*/
|
|
118
|
+
export function mayAskSupportingDocuments(
|
|
119
|
+
config: SupportingDocumentsConfig | undefined | null,
|
|
120
|
+
): boolean {
|
|
121
|
+
if (!config?.enabled) return false;
|
|
122
|
+
return (config.types ?? []).some((entry) => (entry.label ?? '').trim().length > 0);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Whether the step has anything to ask for on this attempt. */
|
|
126
|
+
export function hasSupportingDocumentsStep(
|
|
127
|
+
config: SupportingDocumentsConfig | undefined | null,
|
|
128
|
+
verifiedIds: string[],
|
|
129
|
+
): boolean {
|
|
130
|
+
return resolveSupportingDocuments(config, verifiedIds).length > 0;
|
|
131
|
+
}
|
package/src/config/theme.ts
CHANGED
|
@@ -161,6 +161,22 @@ function toHex({ r, g, b }: Rgb): string {
|
|
|
161
161
|
}
|
|
162
162
|
|
|
163
163
|
/** Alpha-blends `fg` over `bg` at opacity `alpha` (0–1). */
|
|
164
|
+
/**
|
|
165
|
+
* `color` at `alpha`, as an 8-digit hex React Native composites itself.
|
|
166
|
+
*
|
|
167
|
+
* Falls back to blending against `fallbackBg` when the colour is not hex (an
|
|
168
|
+
* `rgb()` or a named colour), which is the old behaviour rather than a crash.
|
|
169
|
+
*/
|
|
170
|
+
function withAlpha(color: string, alpha: number, fallbackBg: string): string {
|
|
171
|
+
const parsed = parseHex(color);
|
|
172
|
+
if (!parsed) return alphaBlend(color, fallbackBg, alpha);
|
|
173
|
+
const byte = Math.round(Math.min(Math.max(alpha, 0), 1) * 255)
|
|
174
|
+
.toString(16)
|
|
175
|
+
.padStart(2, '0');
|
|
176
|
+
const hex = (n: number) => Math.round(n).toString(16).padStart(2, '0');
|
|
177
|
+
return `#${hex(parsed.r)}${hex(parsed.g)}${hex(parsed.b)}${byte}`;
|
|
178
|
+
}
|
|
179
|
+
|
|
164
180
|
function alphaBlend(fg: string, bg: string, alpha: number): string {
|
|
165
181
|
const f = parseHex(fg);
|
|
166
182
|
const b = parseHex(bg);
|
|
@@ -197,9 +213,19 @@ export function applyAppearance(
|
|
|
197
213
|
|
|
198
214
|
if (appearance.primaryColor) {
|
|
199
215
|
next.primary = appearance.primaryColor;
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
216
|
+
// Real alpha, not a pre-blend. These tints are drawn on several different
|
|
217
|
+
// surfaces - the sheet, a card, a pill - and pre-blending has to guess ONE
|
|
218
|
+
// of them. It guessed `background`, so on a card (backgroundSecondary) the
|
|
219
|
+
// 10% tint landed within 3/255 of the card it sat on and the supporting
|
|
220
|
+
// documents step drew its numbered markers invisibly (user report
|
|
221
|
+
// 2026-09-25). Carrying the alpha lets each one composite against whatever
|
|
222
|
+
// it is actually on, which is what the web SDK's `bg-primary/10` does and
|
|
223
|
+
// why it never had this bug. On `background` the result matches the old
|
|
224
|
+
// pre-blend to within 1/255 - an alpha byte cannot hold 0.1 exactly - so
|
|
225
|
+
// nothing that was already correct moves perceptibly.
|
|
226
|
+
next.primary50 = withAlpha(appearance.primaryColor, 0.04, background);
|
|
227
|
+
next.primary100 = withAlpha(appearance.primaryColor, 0.1, background);
|
|
228
|
+
next.primary200 = withAlpha(appearance.primaryColor, 0.2, background);
|
|
203
229
|
}
|
|
204
230
|
if (appearance.accentColor) {
|
|
205
231
|
next.primary100 = appearance.accentColor;
|