@myazahq/kyc-sdk-react-native 2.4.0 → 2.6.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 +151 -5
- package/android/build.gradle +35 -3
- package/android/src/main/java/com/margelo/nitro/myazakyc/HybridMyazaFaceDetector.kt +77 -0
- package/app.plugin.js +89 -8
- package/ios/HybridMyazaFaceDetector.swift +16 -3
- package/nitrogen/generated/android/c++/JHybridMyazaFaceDetectorSpec.cpp +23 -0
- package/nitrogen/generated/android/c++/JHybridMyazaFaceDetectorSpec.hpp +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/myazakyc/HybridMyazaFaceDetectorSpec.kt +9 -0
- package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Bridge.cpp +8 -0
- package/nitrogen/generated/ios/KycSdkReactNative-Swift-Cxx-Bridge.hpp +43 -0
- package/nitrogen/generated/ios/c++/HybridMyazaFaceDetectorSpecSwift.hpp +17 -0
- package/nitrogen/generated/ios/swift/Func_void_bool.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridMyazaFaceDetectorSpec.swift +2 -0
- package/nitrogen/generated/ios/swift/HybridMyazaFaceDetectorSpec_cxx.swift +31 -0
- package/nitrogen/generated/shared/c++/HybridMyazaFaceDetectorSpec.cpp +2 -0
- package/nitrogen/generated/shared/c++/HybridMyazaFaceDetectorSpec.hpp +3 -0
- package/package.json +14 -2
- package/src/MyazaBiometricAuth.tsx +119 -0
- package/src/MyazaKYC.tsx +12 -0
- package/src/components/CountryField.tsx +8 -0
- package/src/components/CountryRegionPicker.tsx +184 -0
- package/src/components/DashedBorder.tsx +55 -0
- package/src/components/DialCodePicker.tsx +53 -67
- package/src/components/DialCodeRow.tsx +102 -0
- package/src/components/FramedMapPicker.tsx +122 -0
- package/src/components/GeoBadge.tsx +34 -0
- package/src/components/Icon.tsx +2 -176
- package/src/components/KycFlow.tsx +60 -32
- package/src/components/LineSkeleton.tsx +100 -0
- package/src/components/MapChrome.tsx +67 -0
- package/src/components/MapPinMarker.tsx +54 -0
- package/src/components/MapPinPicker.tsx +179 -0
- package/src/components/MilestoneTrack.tsx +155 -0
- package/src/components/MultiIdProgress.tsx +107 -0
- package/src/components/MyazaInput.tsx +39 -4
- package/src/components/MyazaSelect.tsx +2 -1
- package/src/components/OptionRow.tsx +22 -3
- package/src/components/OwnershipSlider.tsx +102 -0
- package/src/components/PhoneNumberInput.tsx +45 -4
- package/src/components/PoweredBy.tsx +5 -5
- package/src/components/PresenceBlocks.tsx +153 -0
- package/src/components/StaggerIn.tsx +63 -0
- package/src/components/StepView.tsx +43 -0
- package/src/components/StickyActions.tsx +56 -0
- package/src/components/dialCodeRows.ts +64 -0
- package/src/components/glass/FloatingSheet.tsx +73 -21
- package/src/components/glass/GlassSheet.tsx +14 -2
- package/src/components/icon-map.ts +176 -0
- package/src/components/stepHeaderMeta.tsx +45 -5
- package/src/config/addressCollection.ts +125 -0
- package/src/config/biometricOptions.ts +100 -0
- package/src/config/business.ts +26 -7
- package/src/config/businessDetailsValidity.ts +52 -0
- package/src/config/businessPrefill.ts +72 -0
- package/src/config/businessSteps.ts +20 -3
- package/src/config/consentStep.ts +19 -0
- package/src/config/keyPeople.ts +111 -7
- package/src/config/keyPeopleCorporate.ts +19 -0
- package/src/config/keyPeopleOwnerTypes.ts +12 -0
- package/src/config/keyPeopleOwners.ts +26 -0
- package/src/config/keyPeoplePrefill.ts +138 -0
- package/src/config/keyPeopleSectionDefs.ts +65 -0
- package/src/config/keyPeopleSections.ts +167 -0
- package/src/config/proofOfAddress.ts +54 -11
- package/src/config/regions.ts +24 -0
- package/src/config/registrationHint.ts +1 -1
- package/src/config/stepOrder.ts +100 -6
- package/src/config/theme.ts +8 -2
- package/src/config/uploadLimits.ts +35 -0
- package/src/config/website.ts +36 -0
- package/src/config/workflowMerge.ts +16 -5
- package/src/emrtd/activeAuth.ts +100 -0
- package/src/emrtd/extras.ts +76 -0
- package/src/emrtd/files.ts +5 -0
- package/src/emrtd/index.ts +1 -0
- package/src/emrtd/read.ts +8 -1
- package/src/emrtd/session.ts +41 -0
- package/src/emrtd/stages.ts +11 -0
- package/src/index.ts +54 -1
- package/src/lib/address-current-location.ts +171 -0
- package/src/lib/address-field-modes.ts +119 -0
- package/src/lib/address-flow.ts +172 -0
- package/src/lib/address-helpers.ts +48 -0
- package/src/lib/address-line.ts +76 -0
- package/src/lib/address-pin-move.ts +97 -0
- package/src/lib/address-step-recovery.ts +63 -0
- package/src/lib/authed-image.ts +78 -0
- package/src/lib/biometric-auth.ts +52 -0
- package/src/lib/biometric-copy.ts +51 -0
- package/src/lib/captureRing.ts +83 -0
- package/src/lib/contact-recovery.ts +44 -0
- package/src/lib/country-adoption.ts +89 -0
- package/src/lib/inferred-country.ts +65 -0
- package/src/lib/livenessLayout.ts +49 -0
- package/src/lib/map-frame.ts +159 -0
- package/src/lib/map-tiles.ts +143 -0
- package/src/lib/multi-id.ts +210 -0
- package/src/lib/poa-country-gate.ts +27 -0
- package/src/lib/resubmit.ts +130 -0
- package/src/lib/result-copy.ts +116 -0
- package/src/lib/result-wait.ts +53 -0
- package/src/lib/review-map-surface.ts +26 -0
- package/src/lib/scope.ts +31 -0
- package/src/lib/screen-corners.ts +50 -0
- package/src/lib/selfie-upload-wait.ts +79 -0
- package/src/lib/step-log.ts +24 -3
- package/src/lib/street-view-fov.ts +42 -0
- package/src/lib/use-multi-id-plan.ts +29 -0
- package/src/lib/webview-available.ts +47 -0
- package/src/liveness/useLiveness.ts +6 -1
- package/src/liveness/useModelReady.ts +65 -0
- package/src/liveness/visionCameraFaceDetector.ts +35 -0
- package/src/presence/background-math.ts +100 -0
- package/src/presence/background-store.ts +82 -0
- package/src/presence/background.ts +165 -0
- package/src/presence/foreground-service.ts +193 -0
- package/src/presence/fs.ts +51 -0
- package/src/presence/geofence.ts +41 -0
- package/src/presence/math.ts +44 -0
- package/src/presence/post.ts +40 -0
- package/src/presence/report.ts +87 -0
- package/src/presence/sampler.ts +110 -0
- package/src/presence/status.ts +92 -0
- package/src/presence/store.ts +94 -0
- package/src/presence/tier.ts +36 -0
- package/src/presence/watch-wait.ts +81 -0
- package/src/screens/AddressCountryControl.tsx +115 -0
- package/src/screens/ApplicantRoleStep.tsx +9 -5
- package/src/screens/BusinessCheckPanel.tsx +89 -0
- package/src/screens/BusinessDetailsFields.tsx +112 -0
- package/src/screens/BusinessDetailsStep.tsx +133 -145
- package/src/screens/BusinessDocumentSlot.tsx +5 -5
- package/src/screens/BusinessDocumentsStep.tsx +5 -56
- package/src/screens/BusinessKeyPeopleStep.tsx +103 -71
- package/src/screens/BusinessPickedCard.tsx +86 -0
- package/src/screens/BusinessPickedSection.tsx +51 -0
- package/src/screens/BusinessRegistryPickers.tsx +67 -0
- package/src/screens/BusinessSandboxToggle.tsx +114 -0
- package/src/screens/BusinessSearch.tsx +199 -0
- package/src/screens/BusinessSearchResults.tsx +115 -0
- package/src/screens/CompanyInfoFields.tsx +91 -51
- package/src/screens/ConsentStep.tsx +37 -17
- package/src/screens/ContactActions.tsx +54 -0
- package/src/screens/ContactDestinationField.tsx +5 -0
- package/src/screens/ContactEntryPanel.tsx +75 -0
- package/src/screens/ContactVerificationStep.tsx +43 -42
- package/src/screens/CountrySelectStep.tsx +13 -147
- package/src/screens/IdTypeStep.tsx +12 -5
- package/src/screens/KeepLinksSheet.tsx +88 -0
- package/src/screens/KeyPeopleAwaitCard.tsx +204 -0
- package/src/screens/KeyPeopleAwaitList.tsx +126 -0
- package/src/screens/KeyPeoplePending.tsx +154 -0
- package/src/screens/KeyPeopleSection.tsx +162 -0
- package/src/screens/KeyPeopleSectionsList.tsx +103 -0
- package/src/screens/KeyPeopleUboExemption.tsx +72 -0
- package/src/screens/KeyPersonCard.tsx +36 -4
- package/src/screens/KeyPersonForm.tsx +195 -36
- package/src/screens/KeyPersonKindToggle.tsx +68 -0
- package/src/screens/KeyPersonOwners.tsx +109 -0
- package/src/screens/KeyPersonRoleChips.tsx +68 -0
- package/src/screens/KeyPersonSheet.tsx +92 -108
- package/src/screens/LivenessAvatar.tsx +15 -3
- package/src/screens/LivenessStep.tsx +172 -52
- package/src/screens/NfcStep.tsx +14 -0
- package/src/screens/PoaDocumentTypeList.tsx +66 -0
- package/src/screens/ProofOfAddressParts.tsx +151 -0
- package/src/screens/ProofOfAddressStep.tsx +58 -96
- package/src/screens/SubmittedBadge.tsx +25 -0
- package/src/screens/SubmittedError.tsx +64 -0
- package/src/screens/SubmittedResult.tsx +117 -0
- package/src/screens/SubmittedStep.tsx +82 -119
- package/src/screens/SubmittedSuccess.tsx +127 -0
- package/src/screens/SubmittedWaiting.tsx +45 -0
- package/src/screens/address/AddressEntranceStep.tsx +161 -0
- package/src/screens/address/AddressIntroGate.tsx +148 -0
- package/src/screens/address/AddressMapStub.tsx +50 -0
- package/src/screens/address/AddressPinStep.tsx +178 -0
- package/src/screens/address/AddressReviewStep.tsx +162 -0
- package/src/screens/address/AddressSandboxTabs.tsx +158 -0
- package/src/screens/address/AddressSearchField.tsx +115 -0
- package/src/screens/address/AddressSearchStep.tsx +94 -0
- package/src/screens/address/CurrentLocationRow.tsx +135 -0
- package/src/screens/address/DetailsSheet.tsx +175 -0
- package/src/screens/address/DetailsSheetFields.tsx +184 -0
- package/src/screens/address/EntranceDropzone.tsx +178 -0
- package/src/screens/address/EntranceFraming.tsx +66 -0
- package/src/screens/address/EntrancePills.tsx +60 -0
- package/src/screens/address/FramedStreetView.tsx +155 -0
- package/src/screens/address/IntroDisclosures.tsx +210 -0
- package/src/screens/address/LabelDecisionRow.tsx +113 -0
- package/src/screens/address/PinSummaryRow.tsx +105 -0
- package/src/screens/address/ReviewAddressBand.tsx +111 -0
- package/src/screens/address/ReviewEntranceThumbs.tsx +64 -0
- package/src/screens/address/ReviewMapPicture.tsx +106 -0
- package/src/screens/address/SearchResults.tsx +105 -0
- package/src/screens/address/SearchScreen.tsx +176 -0
- package/src/screens/address/SkipForNow.tsx +43 -0
- package/src/screens/address/StreetViewChrome.tsx +81 -0
- package/src/screens/address/detail-values.ts +22 -0
- package/src/screens/address/fix-source.ts +27 -0
- package/src/screens/address/index.ts +8 -0
- package/src/screens/address/meta.ts +44 -0
- package/src/screens/address/use-address-flow.ts +200 -0
- package/src/screens/address/use-label-pin.ts +80 -0
- package/src/screens/address/use-pin-actions.ts +192 -0
- package/src/screens/biometric/BiometricAuthFlow.tsx +170 -0
- package/src/screens/consent/model.ts +79 -10
- package/src/screens/liveness/CaptureRing.tsx +91 -0
- package/src/screens/liveness/LivenessCamera.tsx +63 -0
- package/src/screens/liveness/LivenessHandover.tsx +43 -0
- package/src/screens/liveness/LivenessOutcome.tsx +4 -3
- package/src/screens/liveness/SelfiePreview.tsx +13 -4
- package/src/screens/liveness/ShutterFlash.tsx +25 -0
- package/src/screens/liveness/index.ts +4 -0
- package/src/screens/liveness/useSelfieUpload.ts +23 -3
- package/src/screens/nfc/NfcReadProgress.tsx +1 -0
- package/src/screens/nfc/NfcScanIllustration.tsx +7 -18
- package/src/screens/useAddressPhotoAttach.ts +77 -0
- package/src/screens/useAwaitingPeople.ts +90 -0
- package/src/screens/useBusinessDocumentAttach.ts +93 -0
- package/src/screens/usePoaAttach.ts +12 -15
- package/src/services/api-biometric.ts +37 -0
- package/src/services/api-types-biometric.ts +41 -0
- package/src/services/api-types.ts +251 -3
- package/src/services/api-verify-types.ts +90 -1
- package/src/services/api.ts +237 -3
- package/src/services/deviceMetadata.ts +1 -1
- package/src/services/errors.ts +10 -2
- package/src/services/location.ts +234 -0
- package/src/services/uploadErrors.ts +30 -0
- package/src/services/workflowGate.ts +8 -0
- package/src/specs/MyazaFaceDetector.nitro.ts +29 -0
- package/src/store/address.ts +97 -0
- package/src/store/businessCheck.ts +113 -0
- package/src/store/derive.ts +44 -4
- package/src/store/kycStore.ts +352 -16
- package/src/store/serverConfig.ts +20 -0
- package/src/store/session.ts +227 -0
- package/src/store/state.ts +289 -2
- package/src/store/submit.ts +80 -13
- package/src/store/submitApplicant.ts +5 -11
- package/src/types/business.ts +23 -2
- package/src/types/config.ts +70 -2
- package/src/types/verification.ts +13 -1
- package/src/types/workflow.ts +112 -1
- package/src/screens/KeyPeopleInviteLinks.tsx +0 -239
|
@@ -20,6 +20,7 @@ namespace margelo::nitro::camera { class HybridFrameSpec; }
|
|
|
20
20
|
#include "FaceResult.hpp"
|
|
21
21
|
#include <memory>
|
|
22
22
|
#include <VisionCamera/HybridFrameSpec.hpp>
|
|
23
|
+
#include <NitroModules/Promise.hpp>
|
|
23
24
|
|
|
24
25
|
#include "KycSdkReactNative-Swift-Cxx-Umbrella.hpp"
|
|
25
26
|
|
|
@@ -79,6 +80,22 @@ namespace margelo::nitro::myazakyc {
|
|
|
79
80
|
auto __value = std::move(__result.value());
|
|
80
81
|
return __value;
|
|
81
82
|
}
|
|
83
|
+
inline bool isModelReady() override {
|
|
84
|
+
auto __result = _swiftPart.isModelReady();
|
|
85
|
+
if (__result.hasError()) [[unlikely]] {
|
|
86
|
+
std::rethrow_exception(__result.error());
|
|
87
|
+
}
|
|
88
|
+
auto __value = std::move(__result.value());
|
|
89
|
+
return __value;
|
|
90
|
+
}
|
|
91
|
+
inline std::shared_ptr<Promise<bool>> prepareModel() override {
|
|
92
|
+
auto __result = _swiftPart.prepareModel();
|
|
93
|
+
if (__result.hasError()) [[unlikely]] {
|
|
94
|
+
std::rethrow_exception(__result.error());
|
|
95
|
+
}
|
|
96
|
+
auto __value = std::move(__result.value());
|
|
97
|
+
return __value;
|
|
98
|
+
}
|
|
82
99
|
|
|
83
100
|
private:
|
|
84
101
|
KycSdkReactNative::HybridMyazaFaceDetectorSpec_cxx _swiftPart;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
///
|
|
2
|
+
/// Func_void_bool.swift
|
|
3
|
+
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
|
|
4
|
+
/// https://github.com/mrousavy/nitro
|
|
5
|
+
/// Copyright © Marc Rousavy @ Margelo
|
|
6
|
+
///
|
|
7
|
+
|
|
8
|
+
import NitroModules
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Wraps a Swift `(_ value: Bool) -> Void` as a class.
|
|
12
|
+
* This class can be used from C++, e.g. to wrap the Swift closure as a `std::function`.
|
|
13
|
+
*/
|
|
14
|
+
public final class Func_void_bool {
|
|
15
|
+
public typealias bridge = margelo.nitro.myazakyc.bridge.swift
|
|
16
|
+
|
|
17
|
+
private let closure: (_ value: Bool) -> Void
|
|
18
|
+
|
|
19
|
+
public init(_ closure: @escaping (_ value: Bool) -> Void) {
|
|
20
|
+
self.closure = closure
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@inline(__always)
|
|
24
|
+
public func call(value: Bool) -> Void {
|
|
25
|
+
self.closure(value)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Casts this instance to a retained unsafe raw pointer.
|
|
30
|
+
* This acquires one additional strong reference on the object!
|
|
31
|
+
*/
|
|
32
|
+
@inline(__always)
|
|
33
|
+
public func toUnsafe() -> UnsafeMutableRawPointer {
|
|
34
|
+
return Unmanaged.passRetained(self).toOpaque()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Casts an unsafe pointer to a `Func_void_bool`.
|
|
39
|
+
* The pointer has to be a retained opaque `Unmanaged<Func_void_bool>`.
|
|
40
|
+
* This removes one strong reference from the object!
|
|
41
|
+
*/
|
|
42
|
+
@inline(__always)
|
|
43
|
+
public static func fromUnsafe(_ pointer: UnsafeMutableRawPointer) -> Func_void_bool {
|
|
44
|
+
return Unmanaged<Func_void_bool>.fromOpaque(pointer).takeRetainedValue()
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -15,6 +15,8 @@ public protocol HybridMyazaFaceDetectorSpec_protocol: HybridObject {
|
|
|
15
15
|
|
|
16
16
|
// Methods
|
|
17
17
|
func detectFace(frame: (any HybridFrameSpec)) throws -> FaceResult
|
|
18
|
+
func isModelReady() throws -> Bool
|
|
19
|
+
func prepareModel() throws -> Promise<Bool>
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
public extension HybridMyazaFaceDetectorSpec_protocol {
|
|
@@ -140,4 +140,35 @@ open class HybridMyazaFaceDetectorSpec_cxx {
|
|
|
140
140
|
return bridge.create_Result_FaceResult_(__exceptionPtr)
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
|
+
|
|
144
|
+
@inline(__always)
|
|
145
|
+
public final func isModelReady() -> bridge.Result_bool_ {
|
|
146
|
+
do {
|
|
147
|
+
let __result = try self.__implementation.isModelReady()
|
|
148
|
+
let __resultCpp = __result
|
|
149
|
+
return bridge.create_Result_bool_(__resultCpp)
|
|
150
|
+
} catch (let __error) {
|
|
151
|
+
let __exceptionPtr = __error.toCpp()
|
|
152
|
+
return bridge.create_Result_bool_(__exceptionPtr)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
@inline(__always)
|
|
157
|
+
public final func prepareModel() -> bridge.Result_std__shared_ptr_Promise_bool___ {
|
|
158
|
+
do {
|
|
159
|
+
let __result = try self.__implementation.prepareModel()
|
|
160
|
+
let __resultCpp = { () -> bridge.std__shared_ptr_Promise_bool__ in
|
|
161
|
+
let __promise = bridge.create_std__shared_ptr_Promise_bool__()
|
|
162
|
+
let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_bool__(__promise)
|
|
163
|
+
__result
|
|
164
|
+
.then({ __result in __promiseHolder.resolve(__result) })
|
|
165
|
+
.catch({ __error in __promiseHolder.reject(__error.toCpp()) })
|
|
166
|
+
return __promise
|
|
167
|
+
}()
|
|
168
|
+
return bridge.create_Result_std__shared_ptr_Promise_bool___(__resultCpp)
|
|
169
|
+
} catch (let __error) {
|
|
170
|
+
let __exceptionPtr = __error.toCpp()
|
|
171
|
+
return bridge.create_Result_std__shared_ptr_Promise_bool___(__exceptionPtr)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
143
174
|
}
|
|
@@ -15,6 +15,8 @@ namespace margelo::nitro::myazakyc {
|
|
|
15
15
|
// load custom methods/properties
|
|
16
16
|
registerHybrids(this, [](Prototype& prototype) {
|
|
17
17
|
prototype.registerHybridMethod("detectFace", &HybridMyazaFaceDetectorSpec::detectFace);
|
|
18
|
+
prototype.registerHybridMethod("isModelReady", &HybridMyazaFaceDetectorSpec::isModelReady);
|
|
19
|
+
prototype.registerHybridMethod("prepareModel", &HybridMyazaFaceDetectorSpec::prepareModel);
|
|
18
20
|
});
|
|
19
21
|
}
|
|
20
22
|
|
|
@@ -21,6 +21,7 @@ namespace margelo::nitro::camera { class HybridFrameSpec; }
|
|
|
21
21
|
#include "FaceResult.hpp"
|
|
22
22
|
#include <memory>
|
|
23
23
|
#include <VisionCamera/HybridFrameSpec.hpp>
|
|
24
|
+
#include <NitroModules/Promise.hpp>
|
|
24
25
|
|
|
25
26
|
namespace margelo::nitro::myazakyc {
|
|
26
27
|
|
|
@@ -54,6 +55,8 @@ namespace margelo::nitro::myazakyc {
|
|
|
54
55
|
public:
|
|
55
56
|
// Methods
|
|
56
57
|
virtual FaceResult detectFace(const std::shared_ptr<margelo::nitro::camera::HybridFrameSpec>& frame) = 0;
|
|
58
|
+
virtual bool isModelReady() = 0;
|
|
59
|
+
virtual std::shared_ptr<Promise<bool>> prepareModel() = 0;
|
|
57
60
|
|
|
58
61
|
protected:
|
|
59
62
|
// Hybrid Setup
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myazahq/kyc-sdk-react-native",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"description": "Myaza KYC SDK for React Native (Expo) — ID verification, liveness detection, and document capture",
|
|
5
5
|
"author": "Flitstack Technologies Inc.",
|
|
6
6
|
"repository": {
|
|
@@ -49,6 +49,8 @@
|
|
|
49
49
|
"license": "MIT",
|
|
50
50
|
"peerDependencies": {
|
|
51
51
|
"expo": ">=56",
|
|
52
|
+
"expo-task-manager": ">=14",
|
|
53
|
+
"react-native-webview": ">=13",
|
|
52
54
|
"react": ">=19",
|
|
53
55
|
"react-native": ">=0.83",
|
|
54
56
|
"react-native-nitro-image": ">=0.15",
|
|
@@ -58,6 +60,14 @@
|
|
|
58
60
|
"react-native-vision-camera-worklets": ">=5",
|
|
59
61
|
"react-native-worklets": ">=0.8"
|
|
60
62
|
},
|
|
63
|
+
"peerDependenciesMeta": {
|
|
64
|
+
"expo-task-manager": {
|
|
65
|
+
"optional": true
|
|
66
|
+
},
|
|
67
|
+
"react-native-webview": {
|
|
68
|
+
"optional": true
|
|
69
|
+
}
|
|
70
|
+
},
|
|
61
71
|
"dependencies": {
|
|
62
72
|
"@expo-google-fonts/karla": "^0.4.2",
|
|
63
73
|
"@expo-google-fonts/space-grotesk": "^0.4.1",
|
|
@@ -74,11 +84,13 @@
|
|
|
74
84
|
"expo-image-manipulator": "~56.0.17",
|
|
75
85
|
"expo-image-picker": "~56.0.16",
|
|
76
86
|
"expo-localization": "~56.0.6",
|
|
87
|
+
"expo-location": "~56.0.24",
|
|
77
88
|
"expo-speech": "~56.0.3",
|
|
78
89
|
"lucide-react-native": "^1.17.0",
|
|
79
90
|
"react-native-compressor": "^1.19.0",
|
|
80
91
|
"react-native-vision-camera": "^5.0.11",
|
|
81
|
-
"zustand": "^5.0.2"
|
|
92
|
+
"zustand": "^5.0.2",
|
|
93
|
+
"expo-file-system": "~56.0.0"
|
|
82
94
|
},
|
|
83
95
|
"devDependencies": {
|
|
84
96
|
"@types/jest": "^29.5.12",
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import React, { useCallback, useState } from 'react';
|
|
2
|
+
import { Modal, Platform } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { KycRuntimeProvider, MyazaThemeProvider } from './components/runtime';
|
|
5
|
+
import { MyazaButton } from './components/MyazaButton';
|
|
6
|
+
import { BiometricAuthFlow } from './screens/biometric/BiometricAuthFlow';
|
|
7
|
+
import { primeFaceModel } from './liveness/visionCameraFaceDetector';
|
|
8
|
+
import { defaultReauthLabel } from './lib/biometric-auth';
|
|
9
|
+
import type { KYCAppearance, ResolvedKYCConfig, SupportedCountry } from './types/config';
|
|
10
|
+
import type { KYCError } from './types/verification';
|
|
11
|
+
import type { BiometricAuthResponse } from './services/api-types-biometric';
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// MyazaBiometricAuth — returning-user face re-authentication ("prove it's
|
|
15
|
+
// still you"). Mirrors the web SDK's MyazaBiometricAuth: a verified user
|
|
16
|
+
// re-authenticates with a live selfie matched 1:1 against their enrollment
|
|
17
|
+
// reference (no gov-DB call, no re-KYC). Self-contained: its own trigger +
|
|
18
|
+
// modal, hosting the SDK's real liveness step inside a scoped runtime.
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
export interface MyazaBiometricAuthProps {
|
|
22
|
+
/** Publishable API key (`pk_…`). The environment is derived from its prefix. */
|
|
23
|
+
apiKey: string;
|
|
24
|
+
/** Dev-only base-URL override for `pk_dev_` keys. */
|
|
25
|
+
devUrl?: string;
|
|
26
|
+
/** The org's user reference (Entity.externalUserId) to re-authenticate. */
|
|
27
|
+
externalUserId: string;
|
|
28
|
+
/** Presence Intelligence method (default 'gestures'). */
|
|
29
|
+
livenessMode?: 'gestures' | 'flash' | 'both';
|
|
30
|
+
flashSequenceLength?: number;
|
|
31
|
+
appearance?: KYCAppearance;
|
|
32
|
+
disableClose?: boolean;
|
|
33
|
+
/** Open the modal on mount (no trigger button is rendered). */
|
|
34
|
+
defaultOpen?: boolean;
|
|
35
|
+
/** Custom trigger label (default "Verify it's you"). */
|
|
36
|
+
children?: string;
|
|
37
|
+
disabled?: boolean;
|
|
38
|
+
onOpen?: () => void;
|
|
39
|
+
/** The user re-authenticated. `token` is a single-use proof — verify it from
|
|
40
|
+
* your backend with a secret key at `/biometric/verify-proof`. */
|
|
41
|
+
onAuthenticated?: (result: { attemptId: string; confidence: number | null; token?: string }) => void;
|
|
42
|
+
/** The check ran but the user did not pass (no match / liveness failed). */
|
|
43
|
+
onFailed?: (result: { status: BiometricAuthResponse['status']; attemptId: string; confidence: number | null }) => void;
|
|
44
|
+
/** A technical error (network, not enrolled, insufficient credits, …). */
|
|
45
|
+
onError?: (error: KYCError) => void;
|
|
46
|
+
onClose?: () => void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const MODAL_PRESENTATION = Platform.OS === 'ios' ? 'pageSheet' : 'fullScreen';
|
|
50
|
+
|
|
51
|
+
export function MyazaBiometricAuth(props: MyazaBiometricAuthProps): React.ReactElement {
|
|
52
|
+
const { children, disabled, defaultOpen, onOpen, onAuthenticated, onFailed, onError, onClose } = props;
|
|
53
|
+
const [open, setOpen] = useState(defaultOpen === true);
|
|
54
|
+
|
|
55
|
+
// The liveness step reads exactly the keys a KYC flow's config carries; the
|
|
56
|
+
// scope parks the step order on consent → liveness → submitted, and the
|
|
57
|
+
// flow only ever mounts the middle one. `country` is required by the
|
|
58
|
+
// resolved shape but nothing on this path reads it.
|
|
59
|
+
const config: ResolvedKYCConfig = {
|
|
60
|
+
apiKey: props.apiKey,
|
|
61
|
+
devUrl: props.devUrl,
|
|
62
|
+
country: 'NG' as SupportedCountry,
|
|
63
|
+
scope: 'biometric-authentication',
|
|
64
|
+
enableSelfie: true,
|
|
65
|
+
livenessMode: props.livenessMode ?? 'gestures',
|
|
66
|
+
flashSequenceLength: props.flashSequenceLength,
|
|
67
|
+
appearance: props.appearance,
|
|
68
|
+
disableClose: props.disableClose,
|
|
69
|
+
userId: props.externalUserId,
|
|
70
|
+
onError,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const openFlow = useCallback(() => {
|
|
74
|
+
void primeFaceModel();
|
|
75
|
+
setOpen(true);
|
|
76
|
+
onOpen?.();
|
|
77
|
+
}, [onOpen]);
|
|
78
|
+
const close = useCallback(() => {
|
|
79
|
+
setOpen(false);
|
|
80
|
+
onClose?.();
|
|
81
|
+
}, [onClose]);
|
|
82
|
+
const blockDismiss = props.disableClose === true;
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<MyazaThemeProvider appearance={props.appearance}>
|
|
86
|
+
{defaultOpen !== true && (
|
|
87
|
+
<MyazaButton
|
|
88
|
+
label={children ?? defaultReauthLabel(props.appearance?.companyName)}
|
|
89
|
+
disabled={disabled}
|
|
90
|
+
fullWidth={false}
|
|
91
|
+
onPress={openFlow}
|
|
92
|
+
/>
|
|
93
|
+
)}
|
|
94
|
+
<Modal
|
|
95
|
+
visible={open}
|
|
96
|
+
animationType="slide"
|
|
97
|
+
presentationStyle={blockDismiss ? 'fullScreen' : MODAL_PRESENTATION}
|
|
98
|
+
statusBarTranslucent
|
|
99
|
+
navigationBarTranslucent
|
|
100
|
+
onRequestClose={blockDismiss ? () => undefined : close}
|
|
101
|
+
onDismiss={blockDismiss ? undefined : close}
|
|
102
|
+
>
|
|
103
|
+
{open ? (
|
|
104
|
+
// A fresh store per open: the runtime provider creates it once per
|
|
105
|
+
// mount, and the modal's children mount once per open.
|
|
106
|
+
<KycRuntimeProvider config={config}>
|
|
107
|
+
<BiometricAuthFlow
|
|
108
|
+
externalUserId={props.externalUserId}
|
|
109
|
+
onAuthenticated={(r) => onAuthenticated?.({ attemptId: r.attemptId, confidence: r.confidence, token: r.token })}
|
|
110
|
+
onFailed={(r) => onFailed?.({ status: r.status, attemptId: r.attemptId, confidence: r.confidence })}
|
|
111
|
+
onError={onError}
|
|
112
|
+
onClose={close}
|
|
113
|
+
/>
|
|
114
|
+
</KycRuntimeProvider>
|
|
115
|
+
) : null}
|
|
116
|
+
</Modal>
|
|
117
|
+
</MyazaThemeProvider>
|
|
118
|
+
);
|
|
119
|
+
}
|
package/src/MyazaKYC.tsx
CHANGED
|
@@ -16,6 +16,7 @@ import { KycRuntimeProvider, MyazaThemeProvider } from './components/runtime';
|
|
|
16
16
|
import { WorkflowGate } from './components/WorkflowGate';
|
|
17
17
|
import { useWorkflowMount } from './components/useWorkflowMount';
|
|
18
18
|
import { safeReportError } from './services/errors';
|
|
19
|
+
import { primeFaceModel } from './liveness/visionCameraFaceDetector';
|
|
19
20
|
import { KycFlow, type BackResult } from './components/KycFlow';
|
|
20
21
|
import { MyazaButton } from './components/MyazaButton';
|
|
21
22
|
|
|
@@ -98,6 +99,17 @@ function MyazaKYCTrigger({
|
|
|
98
99
|
const settled = state.status !== 'resolving';
|
|
99
100
|
const open = wantOpen && settled;
|
|
100
101
|
|
|
102
|
+
// Start fetching the face model the moment the user opens the flow, so the
|
|
103
|
+
// download overlaps consent and ID-type selection rather than stalling in
|
|
104
|
+
// front of the camera. Mirrors the web SDK's primeFaceMesh() on mount; the
|
|
105
|
+
// liveness step still gates on isFaceModelReady(), so this is best-effort.
|
|
106
|
+
const primedRef = useRef(false);
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
if (!wantOpen || primedRef.current) return;
|
|
109
|
+
primedRef.current = true;
|
|
110
|
+
primeFaceModel();
|
|
111
|
+
}, [wantOpen]);
|
|
112
|
+
|
|
101
113
|
// Reported only once the user has actually tried to start. Prefetching must
|
|
102
114
|
// not fire a consumer's error handler for a flow they never opened.
|
|
103
115
|
const reportedRef = useRef(false);
|
|
@@ -21,6 +21,8 @@ export function CountryField({
|
|
|
21
21
|
value,
|
|
22
22
|
options,
|
|
23
23
|
onChange,
|
|
24
|
+
geoCountry,
|
|
25
|
+
grouped,
|
|
24
26
|
placeholder = 'Select a country',
|
|
25
27
|
searchPlaceholder = 'Search country',
|
|
26
28
|
}: {
|
|
@@ -29,6 +31,10 @@ export function CountryField({
|
|
|
29
31
|
/** The pickable countries — all ISO, or a workflow's registry subset. */
|
|
30
32
|
options: DialCodeOption[];
|
|
31
33
|
onChange: (code: string) => void;
|
|
34
|
+
/** The visitor's inferred country, pinned on top of the sheet as "Your location". */
|
|
35
|
+
geoCountry?: string | null;
|
|
36
|
+
/** Region headers between the rows, the country-select step's way. */
|
|
37
|
+
grouped?: boolean;
|
|
32
38
|
placeholder?: string;
|
|
33
39
|
searchPlaceholder?: string;
|
|
34
40
|
}): React.ReactElement {
|
|
@@ -75,6 +81,8 @@ export function CountryField({
|
|
|
75
81
|
visible={open}
|
|
76
82
|
options={options}
|
|
77
83
|
selected={value ?? ''}
|
|
84
|
+
pinned={geoCountry}
|
|
85
|
+
grouped={grouped}
|
|
78
86
|
searchPlaceholder={searchPlaceholder}
|
|
79
87
|
onPick={(code) => {
|
|
80
88
|
setOpen(false);
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import React, { useMemo, useState } from 'react';
|
|
2
|
+
import { Pressable, ScrollView, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { radius, spacing } from '../config/theme';
|
|
5
|
+
import { groupCountriesByRegion, pinGeoRow, regionCountryName } from '../config/regions';
|
|
6
|
+
import { useTheme } from './runtime';
|
|
7
|
+
import { MyazaText } from './Typography';
|
|
8
|
+
import { MyazaInput } from './MyazaInput';
|
|
9
|
+
import { CountryFlag } from './CountryFlag';
|
|
10
|
+
import { Icon } from './Icon';
|
|
11
|
+
import { GeoBadge } from './GeoBadge';
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Searchable, region-grouped country picker for the country-select step. Used
|
|
15
|
+
// when a workflow offers many countries (Global Documents): a flat 200-country
|
|
16
|
+
// list is a scroll, not a chooser, so this adds a pinned search box and
|
|
17
|
+
// continent headers over a bounded scroll area. Mirrors the web SDK's
|
|
18
|
+
// CountryRegionPicker and Flutter's country_region_picker.dart.
|
|
19
|
+
//
|
|
20
|
+
// Split from CountrySelectStep (200-line rule); the step keeps the flat list
|
|
21
|
+
// and the threshold that decides between the two.
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
export function CountryRegionPicker({
|
|
25
|
+
countries,
|
|
26
|
+
selected,
|
|
27
|
+
geoCountry,
|
|
28
|
+
onPick,
|
|
29
|
+
}: {
|
|
30
|
+
countries: string[];
|
|
31
|
+
selected: string | null;
|
|
32
|
+
/**
|
|
33
|
+
* The visitor's IP country. Lifted out of its continent to the very top and
|
|
34
|
+
* tagged, so the one country most likely to be theirs is the first thing
|
|
35
|
+
* they see rather than something to scroll for.
|
|
36
|
+
*/
|
|
37
|
+
geoCountry?: string | null;
|
|
38
|
+
onPick: (country: string) => void;
|
|
39
|
+
}): React.ReactElement {
|
|
40
|
+
const { colors } = useTheme();
|
|
41
|
+
const [query, setQuery] = useState('');
|
|
42
|
+
|
|
43
|
+
const { pinned, groups } = useMemo(() => {
|
|
44
|
+
const needle = query.trim().toLowerCase();
|
|
45
|
+
const visible = needle
|
|
46
|
+
? countries.filter(
|
|
47
|
+
(code) =>
|
|
48
|
+
code.toLowerCase().includes(needle) ||
|
|
49
|
+
regionCountryName(code).toLowerCase().includes(needle),
|
|
50
|
+
)
|
|
51
|
+
: countries;
|
|
52
|
+
const split = pinGeoRow(visible, geoCountry, (code) => code);
|
|
53
|
+
return { pinned: split.pinned, groups: groupCountriesByRegion(split.rest) };
|
|
54
|
+
}, [countries, query, geoCountry]);
|
|
55
|
+
|
|
56
|
+
const empty = groups.length === 0 && pinned === null;
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
// The search box stays put and the list owns its own scroll, so a
|
|
60
|
+
// ~240-country flow does not scroll the search field off the top. That
|
|
61
|
+
// relies on the SHEET rendering this step with `fillsViewport` — inside a
|
|
62
|
+
// scroll view the nested list below would take an unbounded height, outgrow
|
|
63
|
+
// the viewport, and drag the search box off the top with it.
|
|
64
|
+
<View style={{ flex: 1 }}>
|
|
65
|
+
<MyazaInput
|
|
66
|
+
value={query}
|
|
67
|
+
onChangeText={setQuery}
|
|
68
|
+
placeholder="Search countries…"
|
|
69
|
+
autoCapitalize="none"
|
|
70
|
+
prefix={<Icon name="search" size={18} color={colors.textSecondary} />}
|
|
71
|
+
/>
|
|
72
|
+
<View style={{ height: spacing.md }} />
|
|
73
|
+
|
|
74
|
+
{empty ? (
|
|
75
|
+
<MyazaText variant="bodyMedium" style={{ textAlign: 'center', marginTop: spacing.lg }}>
|
|
76
|
+
No countries match your search.
|
|
77
|
+
</MyazaText>
|
|
78
|
+
) : (
|
|
79
|
+
// `nestedScrollEnabled` stays: Android refuses to scroll a nested
|
|
80
|
+
// same-direction list without it, and this is still nested on the
|
|
81
|
+
// flat-list path and inside any future scrolling parent.
|
|
82
|
+
<ScrollView
|
|
83
|
+
nestedScrollEnabled
|
|
84
|
+
keyboardShouldPersistTaps="handled"
|
|
85
|
+
showsVerticalScrollIndicator={false}
|
|
86
|
+
>
|
|
87
|
+
{pinned ? (
|
|
88
|
+
<CountryRow
|
|
89
|
+
code={pinned}
|
|
90
|
+
name={regionCountryName(pinned)}
|
|
91
|
+
selected={pinned === selected}
|
|
92
|
+
badge="Your location"
|
|
93
|
+
onPress={() => onPick(pinned)}
|
|
94
|
+
/>
|
|
95
|
+
) : null}
|
|
96
|
+
{groups.map((group) => (
|
|
97
|
+
<View key={group.region}>
|
|
98
|
+
{/* Uppercase, tracked-out section label — Flutter's region
|
|
99
|
+
header. Lowercase text at the default weight read as another
|
|
100
|
+
list row rather than a divider. */}
|
|
101
|
+
<MyazaText
|
|
102
|
+
variant="bodySmall"
|
|
103
|
+
color={colors.textSecondary}
|
|
104
|
+
style={{
|
|
105
|
+
fontWeight: '700',
|
|
106
|
+
letterSpacing: 0.6,
|
|
107
|
+
paddingHorizontal: spacing.xs,
|
|
108
|
+
paddingVertical: spacing.sm,
|
|
109
|
+
}}
|
|
110
|
+
>
|
|
111
|
+
{group.region.toUpperCase()}
|
|
112
|
+
</MyazaText>
|
|
113
|
+
{group.countries.map((entry) => (
|
|
114
|
+
<CountryRow
|
|
115
|
+
key={entry.code}
|
|
116
|
+
code={entry.code}
|
|
117
|
+
name={entry.name}
|
|
118
|
+
selected={entry.code === selected}
|
|
119
|
+
onPress={() => onPick(entry.code)}
|
|
120
|
+
/>
|
|
121
|
+
))}
|
|
122
|
+
</View>
|
|
123
|
+
))}
|
|
124
|
+
</ScrollView>
|
|
125
|
+
)}
|
|
126
|
+
</View>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* One selectable country row, shared by the flat list and the region picker
|
|
132
|
+
* so the two cannot drift apart. `badge` tags the pinned geo row; it is the
|
|
133
|
+
* same control in a second position, differing only by the tag.
|
|
134
|
+
*/
|
|
135
|
+
export function CountryRow({
|
|
136
|
+
code,
|
|
137
|
+
name,
|
|
138
|
+
selected,
|
|
139
|
+
badge,
|
|
140
|
+
onPress,
|
|
141
|
+
}: {
|
|
142
|
+
code: string;
|
|
143
|
+
name: string;
|
|
144
|
+
selected: boolean;
|
|
145
|
+
badge?: string;
|
|
146
|
+
onPress: () => void;
|
|
147
|
+
}): React.ReactElement {
|
|
148
|
+
const { colors } = useTheme();
|
|
149
|
+
return (
|
|
150
|
+
<Pressable
|
|
151
|
+
onPress={onPress}
|
|
152
|
+
accessibilityRole="radio"
|
|
153
|
+
accessibilityState={{ checked: selected }}
|
|
154
|
+
accessibilityLabel={badge ? `${name}, ${badge.toLowerCase()}` : name}
|
|
155
|
+
// Matches the Flutter SDK's CountryOptionTile exactly — the two had
|
|
156
|
+
// drifted (24px flag / 12px padding here vs 32 / 16 there), which is why
|
|
157
|
+
// the same screen looked like a different component on each platform. The
|
|
158
|
+
// spacing and radius scales are identical across the SDKs, so there is
|
|
159
|
+
// nothing platform-specific to reconcile: `md` means 16 on both.
|
|
160
|
+
style={{
|
|
161
|
+
flexDirection: 'row',
|
|
162
|
+
alignItems: 'center',
|
|
163
|
+
padding: spacing.md,
|
|
164
|
+
borderRadius: radius.md,
|
|
165
|
+
borderWidth: selected ? 1.5 : 1,
|
|
166
|
+
borderColor: selected ? colors.primary : colors.border,
|
|
167
|
+
backgroundColor: selected ? colors.primary50 : colors.background,
|
|
168
|
+
marginBottom: spacing.sm,
|
|
169
|
+
}}
|
|
170
|
+
>
|
|
171
|
+
<CountryFlag country={code} size={32} />
|
|
172
|
+
<View style={{ width: 12 }} />
|
|
173
|
+
<MyazaText variant="label" style={{ flex: 1, fontWeight: '500' }}>
|
|
174
|
+
{name}
|
|
175
|
+
</MyazaText>
|
|
176
|
+
{badge ? <GeoBadge label={badge} /> : null}
|
|
177
|
+
<Icon
|
|
178
|
+
name="chevron-right"
|
|
179
|
+
size={18}
|
|
180
|
+
color={selected ? colors.primary : colors.textSecondary}
|
|
181
|
+
/>
|
|
182
|
+
</Pressable>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
import Svg, { Rect } from 'react-native-svg';
|
|
4
|
+
|
|
5
|
+
// ─── A dashed card border the platform cannot restyle ─────────────────────────
|
|
6
|
+
//
|
|
7
|
+
// Native `borderStyle: 'dashed'` is drawn by the OS, and iOS draws it with
|
|
8
|
+
// long dashes and wide gaps that nothing can tune — beside the web SDK's
|
|
9
|
+
// tight CSS pattern the same card looked hand-drawn. This draws the same
|
|
10
|
+
// rectangle with SVG, where the dash pattern is ours to set; the defaults
|
|
11
|
+
// match the web look (and the Flutter SDK's DashedRoundedBorder). Overlay it
|
|
12
|
+
// inside a relatively-positioned card and drop the native border props.
|
|
13
|
+
|
|
14
|
+
export function DashedBorder({
|
|
15
|
+
color,
|
|
16
|
+
radius,
|
|
17
|
+
strokeWidth = 2,
|
|
18
|
+
dash = 4,
|
|
19
|
+
gap = 3,
|
|
20
|
+
}: {
|
|
21
|
+
color: string;
|
|
22
|
+
radius: number;
|
|
23
|
+
strokeWidth?: number;
|
|
24
|
+
dash?: number;
|
|
25
|
+
gap?: number;
|
|
26
|
+
}): React.ReactElement {
|
|
27
|
+
const [size, setSize] = useState<{ w: number; h: number } | null>(null);
|
|
28
|
+
return (
|
|
29
|
+
<View
|
|
30
|
+
pointerEvents="none"
|
|
31
|
+
style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }}
|
|
32
|
+
onLayout={(e) =>
|
|
33
|
+
setSize({ w: e.nativeEvent.layout.width, h: e.nativeEvent.layout.height })
|
|
34
|
+
}
|
|
35
|
+
>
|
|
36
|
+
{size && size.w > strokeWidth && size.h > strokeWidth ? (
|
|
37
|
+
<Svg width={size.w} height={size.h}>
|
|
38
|
+
{/* Inset by half the stroke so the line sits fully inside the card,
|
|
39
|
+
where a native border would have been. */}
|
|
40
|
+
<Rect
|
|
41
|
+
x={strokeWidth / 2}
|
|
42
|
+
y={strokeWidth / 2}
|
|
43
|
+
width={size.w - strokeWidth}
|
|
44
|
+
height={size.h - strokeWidth}
|
|
45
|
+
rx={radius}
|
|
46
|
+
fill="none"
|
|
47
|
+
stroke={color}
|
|
48
|
+
strokeWidth={strokeWidth}
|
|
49
|
+
strokeDasharray={[dash, gap]}
|
|
50
|
+
/>
|
|
51
|
+
</Svg>
|
|
52
|
+
) : null}
|
|
53
|
+
</View>
|
|
54
|
+
);
|
|
55
|
+
}
|