@kontextso/sdk-react-native 3.3.0 → 3.4.0-rc.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/android/build.gradle +1 -0
- package/android/src/main/AndroidManifest.xml +2 -1
- package/android/src/main/java/so/kontext/react/RNKontextModuleImpl.kt +34 -0
- package/android/src/newarch/java/so/kontext/react/RNKontextModule.kt +16 -0
- package/android/src/oldarch/java/so/kontext/react/RNKontextModule.kt +20 -0
- package/dist/index.js +170 -34
- package/dist/index.mjs +162 -26
- package/ios/KontextSDK.swift +47 -0
- package/ios/PrivacyInfo.xcprivacy +64 -0
- package/ios/RNKontext.mm +40 -0
- package/ios/TrackingAuthorizationManager.swift +46 -0
- package/package.json +2 -2
- package/src/NativeRNKontext.ts +14 -0
- package/src/context/AdsProvider.tsx +33 -1
- package/src/formats/Format.tsx +18 -10
- package/src/frame-webview.tsx +26 -5
- package/src/services/Att.ts +117 -0
package/android/build.gradle
CHANGED
|
@@ -86,4 +86,5 @@ dependencies {
|
|
|
86
86
|
implementation "com.facebook.react:react-android"
|
|
87
87
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
|
88
88
|
implementation "androidx.preference:preference:1.2.1"
|
|
89
|
+
implementation "com.google.android.gms:play-services-ads-identifier:18.0.1"
|
|
89
90
|
}
|
|
@@ -1,16 +1,50 @@
|
|
|
1
1
|
package so.kontext.react
|
|
2
2
|
|
|
3
3
|
import android.content.Context
|
|
4
|
+
import android.os.Handler
|
|
5
|
+
import android.os.Looper
|
|
4
6
|
import android.media.AudioManager
|
|
5
7
|
import androidx.preference.PreferenceManager
|
|
6
8
|
import com.facebook.react.bridge.Arguments
|
|
7
9
|
import com.facebook.react.bridge.Promise
|
|
8
10
|
import com.facebook.react.bridge.ReactApplicationContext
|
|
9
11
|
import com.facebook.react.bridge.WritableMap
|
|
12
|
+
import com.google.android.gms.ads.identifier.AdvertisingIdClient
|
|
13
|
+
import java.util.concurrent.Executors
|
|
10
14
|
|
|
11
15
|
|
|
12
16
|
class RNKontextModuleImpl(private val reactContext: ReactApplicationContext) {
|
|
13
17
|
|
|
18
|
+
private val mainHandler = Handler(Looper.getMainLooper())
|
|
19
|
+
private val executor = Executors.newSingleThreadExecutor()
|
|
20
|
+
|
|
21
|
+
fun getAdvertisingId(promise: Promise?) {
|
|
22
|
+
if (promise == null) return
|
|
23
|
+
executor.execute {
|
|
24
|
+
val id = try {
|
|
25
|
+
val info = AdvertisingIdClient.getAdvertisingIdInfo(reactContext.applicationContext)
|
|
26
|
+
if (info.isLimitAdTrackingEnabled) null else info.id
|
|
27
|
+
} catch (_: Exception) {
|
|
28
|
+
null
|
|
29
|
+
}
|
|
30
|
+
mainHandler.post { promise.resolve(id) }
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Android has no IDFV equivalent — iOS only
|
|
35
|
+
fun getVendorId(promise: Promise?) {
|
|
36
|
+
promise?.resolve(null)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Android has no ATT — iOS only
|
|
40
|
+
fun getTrackingAuthorizationStatus(promise: Promise?) {
|
|
41
|
+
promise?.resolve(4) // TrackingStatus.NotSupported
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
fun requestTrackingAuthorization(promise: Promise?) {
|
|
45
|
+
promise?.resolve(4) // TrackingStatus.NotSupported
|
|
46
|
+
}
|
|
47
|
+
|
|
14
48
|
fun isSoundOn(promise: Promise?) {
|
|
15
49
|
val audioManager = reactContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
|
16
50
|
val current = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
|
|
@@ -12,6 +12,22 @@ class RNKontextModule(reactContext: ReactApplicationContext) :
|
|
|
12
12
|
|
|
13
13
|
override fun getName(): String = NAME
|
|
14
14
|
|
|
15
|
+
override fun getAdvertisingId(promise: Promise?) {
|
|
16
|
+
impl.getAdvertisingId(promise)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
override fun getVendorId(promise: Promise?) {
|
|
20
|
+
impl.getVendorId(promise)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
override fun getTrackingAuthorizationStatus(promise: Promise?) {
|
|
24
|
+
impl.getTrackingAuthorizationStatus(promise)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
override fun requestTrackingAuthorization(promise: Promise?) {
|
|
28
|
+
impl.requestTrackingAuthorization(promise)
|
|
29
|
+
}
|
|
30
|
+
|
|
15
31
|
override fun isSoundOn(promise: Promise?) {
|
|
16
32
|
impl.isSoundOn(promise)
|
|
17
33
|
}
|
|
@@ -14,6 +14,26 @@ class RNKontextModule(reactContext: ReactApplicationContext) :
|
|
|
14
14
|
|
|
15
15
|
override fun getName(): String = NAME
|
|
16
16
|
|
|
17
|
+
@ReactMethod
|
|
18
|
+
fun getAdvertisingId(promise: Promise?) {
|
|
19
|
+
impl.getAdvertisingId(promise)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@ReactMethod
|
|
23
|
+
fun getVendorId(promise: Promise?) {
|
|
24
|
+
impl.getVendorId(promise)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@ReactMethod
|
|
28
|
+
fun getTrackingAuthorizationStatus(promise: Promise?) {
|
|
29
|
+
impl.getTrackingAuthorizationStatus(promise)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@ReactMethod
|
|
33
|
+
fun requestTrackingAuthorization(promise: Promise?) {
|
|
34
|
+
impl.requestTrackingAuthorization(promise)
|
|
35
|
+
}
|
|
36
|
+
|
|
17
37
|
@ReactMethod
|
|
18
38
|
fun isSoundOn(promise: Promise?) {
|
|
19
39
|
impl.isSoundOn(promise)
|
package/dist/index.js
CHANGED
|
@@ -57,14 +57,16 @@ function handleIframeMessage(handler, opts) {
|
|
|
57
57
|
// src/formats/Format.tsx
|
|
58
58
|
var import_sdk_react = require("@kontextso/sdk-react");
|
|
59
59
|
var import_react2 = require("react");
|
|
60
|
-
var
|
|
60
|
+
var import_react_native5 = require("react-native");
|
|
61
61
|
|
|
62
62
|
// src/frame-webview.tsx
|
|
63
63
|
var import_react = require("react");
|
|
64
|
+
var import_react_native = require("react-native");
|
|
64
65
|
var import_react_native_webview = require("react-native-webview");
|
|
65
66
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
66
67
|
var FrameWebView = (0, import_react.forwardRef)(
|
|
67
68
|
({ iframeUrl, onMessage, style, onError, onLoad }, forwardedRef) => {
|
|
69
|
+
const isLoadedRef = (0, import_react.useRef)(false);
|
|
68
70
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
69
71
|
import_react_native_webview.WebView,
|
|
70
72
|
{
|
|
@@ -89,9 +91,25 @@ var FrameWebView = (0, import_react.forwardRef)(
|
|
|
89
91
|
window.ReactNativeWebView.postMessage(JSON.stringify(event.data));
|
|
90
92
|
}
|
|
91
93
|
}, false);
|
|
94
|
+
|
|
92
95
|
`,
|
|
93
|
-
onError
|
|
94
|
-
|
|
96
|
+
onError: (event) => onError(
|
|
97
|
+
new Error(`${event.nativeEvent.title}: ${event.nativeEvent.description} [${event.nativeEvent.url}]`),
|
|
98
|
+
true
|
|
99
|
+
),
|
|
100
|
+
onLoad: () => {
|
|
101
|
+
isLoadedRef.current = true;
|
|
102
|
+
onLoad();
|
|
103
|
+
},
|
|
104
|
+
onShouldStartLoadWithRequest: (request) => {
|
|
105
|
+
if (!isLoadedRef.current || request.url.match(/^about:/) || request.url.startsWith(iframeUrl)) {
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
import_react_native.Linking.openURL(request.url).catch((err) => {
|
|
109
|
+
onError(new Error(`Failed to open URL: ${request.url} [${err.message}]`));
|
|
110
|
+
});
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
95
113
|
}
|
|
96
114
|
);
|
|
97
115
|
}
|
|
@@ -99,11 +117,11 @@ var FrameWebView = (0, import_react.forwardRef)(
|
|
|
99
117
|
var frame_webview_default = FrameWebView;
|
|
100
118
|
|
|
101
119
|
// src/services/SkOverlay.ts
|
|
102
|
-
var
|
|
120
|
+
var import_react_native3 = require("react-native");
|
|
103
121
|
|
|
104
122
|
// src/NativeRNKontext.ts
|
|
105
|
-
var
|
|
106
|
-
var NativeRNKontext_default =
|
|
123
|
+
var import_react_native2 = require("react-native");
|
|
124
|
+
var NativeRNKontext_default = import_react_native2.TurboModuleRegistry.getEnforcing("RNKontext");
|
|
107
125
|
|
|
108
126
|
// src/services/utils.ts
|
|
109
127
|
var isValidAppStoreId = (id) => {
|
|
@@ -115,7 +133,7 @@ var isValidPosition = (p) => {
|
|
|
115
133
|
return p === "bottom" || p === "bottomRaised";
|
|
116
134
|
};
|
|
117
135
|
async function presentSKOverlay(params) {
|
|
118
|
-
if (
|
|
136
|
+
if (import_react_native3.Platform.OS !== "ios") {
|
|
119
137
|
return false;
|
|
120
138
|
}
|
|
121
139
|
let { appStoreId, position, dismissible } = params;
|
|
@@ -131,16 +149,16 @@ async function presentSKOverlay(params) {
|
|
|
131
149
|
return NativeRNKontext_default.presentSKOverlay(appStoreId, position, dismissible);
|
|
132
150
|
}
|
|
133
151
|
async function dismissSKOverlay() {
|
|
134
|
-
if (
|
|
152
|
+
if (import_react_native3.Platform.OS !== "ios") {
|
|
135
153
|
return false;
|
|
136
154
|
}
|
|
137
155
|
return NativeRNKontext_default.dismissSKOverlay();
|
|
138
156
|
}
|
|
139
157
|
|
|
140
158
|
// src/services/SkStoreProduct.ts
|
|
141
|
-
var
|
|
159
|
+
var import_react_native4 = require("react-native");
|
|
142
160
|
async function presentSKStoreProduct(appStoreId) {
|
|
143
|
-
if (
|
|
161
|
+
if (import_react_native4.Platform.OS !== "ios") {
|
|
144
162
|
return false;
|
|
145
163
|
}
|
|
146
164
|
if (!isValidAppStoreId(appStoreId)) {
|
|
@@ -149,7 +167,7 @@ async function presentSKStoreProduct(appStoreId) {
|
|
|
149
167
|
return NativeRNKontext_default.presentSKStoreProduct(appStoreId);
|
|
150
168
|
}
|
|
151
169
|
async function dismissSKStoreProduct() {
|
|
152
|
-
if (
|
|
170
|
+
if (import_react_native4.Platform.OS !== "ios") {
|
|
153
171
|
return false;
|
|
154
172
|
}
|
|
155
173
|
return NativeRNKontext_default.dismissSKStoreProduct();
|
|
@@ -194,7 +212,7 @@ var Format = ({ code, messageId, wrapper, onEvent, ...otherParams }) => {
|
|
|
194
212
|
const messageStatusRef = (0, import_react2.useRef)("none" /* None */);
|
|
195
213
|
const modalInitTimeoutRef = (0, import_react2.useRef)(null);
|
|
196
214
|
const isModalInitRef = (0, import_react2.useRef)(false);
|
|
197
|
-
const { height: windowHeight, width: windowWidth } = (0,
|
|
215
|
+
const { height: windowHeight, width: windowWidth } = (0, import_react_native5.useWindowDimensions)();
|
|
198
216
|
const keyboardHeightRef = (0, import_react2.useRef)(0);
|
|
199
217
|
const isAdViewVisible = showIframe && iframeLoaded;
|
|
200
218
|
const reset = () => {
|
|
@@ -205,7 +223,6 @@ var Format = ({ code, messageId, wrapper, onEvent, ...otherParams }) => {
|
|
|
205
223
|
setIframeLoaded(false);
|
|
206
224
|
resetModal();
|
|
207
225
|
context?.resetAll();
|
|
208
|
-
context?.captureError(new Error("Processing iframe error"));
|
|
209
226
|
};
|
|
210
227
|
const resetModal = () => {
|
|
211
228
|
if (modalInitTimeoutRef.current) {
|
|
@@ -316,7 +333,7 @@ var Format = ({ code, messageId, wrapper, onEvent, ...otherParams }) => {
|
|
|
316
333
|
return;
|
|
317
334
|
}
|
|
318
335
|
try {
|
|
319
|
-
await
|
|
336
|
+
await import_react_native5.Linking.openURL(`${context?.adServerUrl}${message.data.url}`);
|
|
320
337
|
} catch (e) {
|
|
321
338
|
console.error("error opening url", e);
|
|
322
339
|
}
|
|
@@ -451,7 +468,6 @@ var Format = ({ code, messageId, wrapper, onEvent, ...otherParams }) => {
|
|
|
451
468
|
case "error-component-iframe":
|
|
452
469
|
case "error-iframe":
|
|
453
470
|
resetModal();
|
|
454
|
-
context?.captureError(new Error("Processing modal iframe error"));
|
|
455
471
|
break;
|
|
456
472
|
case "open-skoverlay-iframe":
|
|
457
473
|
openSkOverlay(
|
|
@@ -550,10 +566,10 @@ var Format = ({ code, messageId, wrapper, onEvent, ...otherParams }) => {
|
|
|
550
566
|
return () => clearInterval(interval);
|
|
551
567
|
}, [isAdViewVisible]);
|
|
552
568
|
(0, import_react2.useEffect)(() => {
|
|
553
|
-
const showSubscription =
|
|
569
|
+
const showSubscription = import_react_native5.Keyboard.addListener("keyboardDidShow", (e) => {
|
|
554
570
|
keyboardHeightRef.current = e?.endCoordinates?.height ?? 0;
|
|
555
571
|
});
|
|
556
|
-
const hideSubscription =
|
|
572
|
+
const hideSubscription = import_react_native5.Keyboard.addListener("keyboardDidHide", () => {
|
|
557
573
|
keyboardHeightRef.current = 0;
|
|
558
574
|
});
|
|
559
575
|
return () => {
|
|
@@ -578,9 +594,14 @@ var Format = ({ code, messageId, wrapper, onEvent, ...otherParams }) => {
|
|
|
578
594
|
borderWidth: 0,
|
|
579
595
|
...iframeStyles
|
|
580
596
|
},
|
|
581
|
-
onError: () => {
|
|
582
|
-
debug("iframe-error"
|
|
583
|
-
|
|
597
|
+
onError: (error, shouldReset) => {
|
|
598
|
+
debug("iframe-error", {
|
|
599
|
+
error: error.toString()
|
|
600
|
+
});
|
|
601
|
+
context?.captureError(error);
|
|
602
|
+
if (shouldReset) {
|
|
603
|
+
reset();
|
|
604
|
+
}
|
|
584
605
|
},
|
|
585
606
|
onLoad: () => {
|
|
586
607
|
debug("iframe-load");
|
|
@@ -589,7 +610,7 @@ var Format = ({ code, messageId, wrapper, onEvent, ...otherParams }) => {
|
|
|
589
610
|
}
|
|
590
611
|
);
|
|
591
612
|
const interstitialContent = /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
592
|
-
|
|
613
|
+
import_react_native5.Modal,
|
|
593
614
|
{
|
|
594
615
|
visible: modalOpen,
|
|
595
616
|
transparent: true,
|
|
@@ -597,7 +618,7 @@ var Format = ({ code, messageId, wrapper, onEvent, ...otherParams }) => {
|
|
|
597
618
|
animationType: "slide",
|
|
598
619
|
statusBarTranslucent: true,
|
|
599
620
|
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
600
|
-
|
|
621
|
+
import_react_native5.View,
|
|
601
622
|
{
|
|
602
623
|
style: {
|
|
603
624
|
flex: 1,
|
|
@@ -616,9 +637,14 @@ var Format = ({ code, messageId, wrapper, onEvent, ...otherParams }) => {
|
|
|
616
637
|
width: "100%",
|
|
617
638
|
borderWidth: 0
|
|
618
639
|
},
|
|
619
|
-
onError: () => {
|
|
620
|
-
debug("modal-error"
|
|
621
|
-
|
|
640
|
+
onError: (error, shouldReset) => {
|
|
641
|
+
debug("modal-error", {
|
|
642
|
+
error: error.toString()
|
|
643
|
+
});
|
|
644
|
+
context?.captureError(error);
|
|
645
|
+
if (shouldReset) {
|
|
646
|
+
resetModal();
|
|
647
|
+
}
|
|
622
648
|
},
|
|
623
649
|
onLoad: () => {
|
|
624
650
|
debug("modal-load");
|
|
@@ -632,7 +658,7 @@ var Format = ({ code, messageId, wrapper, onEvent, ...otherParams }) => {
|
|
|
632
658
|
);
|
|
633
659
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
|
|
634
660
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
635
|
-
|
|
661
|
+
import_react_native5.View,
|
|
636
662
|
{
|
|
637
663
|
style: isAdViewVisible ? containerStyles : {
|
|
638
664
|
height: 0,
|
|
@@ -658,13 +684,92 @@ var InlineAd_default = InlineAd;
|
|
|
658
684
|
// src/context/AdsProvider.tsx
|
|
659
685
|
var import_sdk_react2 = require("@kontextso/sdk-react");
|
|
660
686
|
var import_netinfo = require("@react-native-community/netinfo");
|
|
661
|
-
var
|
|
687
|
+
var import_react_native7 = require("react-native");
|
|
662
688
|
var import_react_native_device_info = __toESM(require("react-native-device-info"));
|
|
663
689
|
|
|
664
690
|
// package.json
|
|
665
|
-
var version = "3.
|
|
691
|
+
var version = "3.4.0-rc.0";
|
|
692
|
+
|
|
693
|
+
// src/services/Att.ts
|
|
694
|
+
var import_react_native6 = require("react-native");
|
|
695
|
+
var ZERO_UUID = "00000000-0000-0000-0000-000000000000";
|
|
696
|
+
var didRunStartupFlow = false;
|
|
697
|
+
var startupFlowPromise = null;
|
|
698
|
+
function requestTrackingAuthorization() {
|
|
699
|
+
if (didRunStartupFlow) {
|
|
700
|
+
return startupFlowPromise ?? Promise.resolve();
|
|
701
|
+
}
|
|
702
|
+
didRunStartupFlow = true;
|
|
703
|
+
startupFlowPromise = _runStartupFlow();
|
|
704
|
+
return startupFlowPromise;
|
|
705
|
+
}
|
|
706
|
+
async function getTrackingAuthorizationStatus() {
|
|
707
|
+
if (import_react_native6.Platform.OS !== "ios") {
|
|
708
|
+
return 4 /* NotSupported */;
|
|
709
|
+
}
|
|
710
|
+
try {
|
|
711
|
+
const raw = await NativeRNKontext_default.getTrackingAuthorizationStatus();
|
|
712
|
+
return _mapRawStatus(raw);
|
|
713
|
+
} catch (error) {
|
|
714
|
+
console.error(error);
|
|
715
|
+
return 4 /* NotSupported */;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
async function getAdvertisingId() {
|
|
719
|
+
try {
|
|
720
|
+
const id = await NativeRNKontext_default.getAdvertisingId();
|
|
721
|
+
return _normalizeId(id);
|
|
722
|
+
} catch (error) {
|
|
723
|
+
console.error(error);
|
|
724
|
+
return null;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
async function getVendorId() {
|
|
728
|
+
if (import_react_native6.Platform.OS !== "ios") {
|
|
729
|
+
return null;
|
|
730
|
+
}
|
|
731
|
+
try {
|
|
732
|
+
const id = await NativeRNKontext_default.getVendorId();
|
|
733
|
+
return _normalizeId(id);
|
|
734
|
+
} catch (error) {
|
|
735
|
+
console.error(error);
|
|
736
|
+
return null;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
async function resolveIds(fallbacks) {
|
|
740
|
+
const [vendorId, advertisingId] = await Promise.all([getVendorId(), getAdvertisingId()]);
|
|
741
|
+
return {
|
|
742
|
+
vendorId: vendorId ?? _normalizeId(fallbacks?.vendorId),
|
|
743
|
+
advertisingId: advertisingId ?? _normalizeId(fallbacks?.advertisingId)
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
async function _runStartupFlow() {
|
|
747
|
+
if (import_react_native6.Platform.OS !== "ios") return;
|
|
748
|
+
try {
|
|
749
|
+
if (!_isVersionAtLeast145(import_react_native6.Platform.Version)) return;
|
|
750
|
+
const status = await getTrackingAuthorizationStatus();
|
|
751
|
+
if (status !== 0 /* NotDetermined */) return;
|
|
752
|
+
await NativeRNKontext_default.requestTrackingAuthorization();
|
|
753
|
+
} catch (error) {
|
|
754
|
+
console.error(error);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
function _isVersionAtLeast145(version2) {
|
|
758
|
+
const [major = 0, minor = 0] = version2.split(".").map(Number);
|
|
759
|
+
return major > 14 || major === 14 && minor >= 5;
|
|
760
|
+
}
|
|
761
|
+
function _mapRawStatus(raw) {
|
|
762
|
+
if (raw >= 0 && raw <= 4) return raw;
|
|
763
|
+
return 4 /* NotSupported */;
|
|
764
|
+
}
|
|
765
|
+
function _normalizeId(id) {
|
|
766
|
+
if (!id || id.trim() === "") return null;
|
|
767
|
+
if (id.toLowerCase() === ZERO_UUID) return null;
|
|
768
|
+
return id;
|
|
769
|
+
}
|
|
666
770
|
|
|
667
771
|
// src/context/AdsProvider.tsx
|
|
772
|
+
var import_react3 = require("react");
|
|
668
773
|
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
669
774
|
ErrorUtils.setGlobalHandler((error, isFatal) => {
|
|
670
775
|
if (!isFatal) {
|
|
@@ -683,7 +788,7 @@ var getDevice = async () => {
|
|
|
683
788
|
const powerState = await import_react_native_device_info.default.getPowerState();
|
|
684
789
|
const deviceType = import_react_native_device_info.default.getDeviceType();
|
|
685
790
|
const soundOn = await NativeRNKontext_default.isSoundOn();
|
|
686
|
-
const screen =
|
|
791
|
+
const screen = import_react_native7.Dimensions.get("screen");
|
|
687
792
|
const networkInfo = await (0, import_netinfo.fetch)();
|
|
688
793
|
const mapDeviceTypeToHardwareType = () => {
|
|
689
794
|
switch (deviceType) {
|
|
@@ -720,14 +825,14 @@ var getDevice = async () => {
|
|
|
720
825
|
detail: networkInfo.type === import_netinfo.NetInfoStateType.cellular && networkInfo.details.cellularGeneration || void 0
|
|
721
826
|
},
|
|
722
827
|
os: {
|
|
723
|
-
name:
|
|
828
|
+
name: import_react_native7.Platform.OS,
|
|
724
829
|
version: import_react_native_device_info.default.getSystemVersion(),
|
|
725
830
|
locale: Intl.DateTimeFormat().resolvedOptions().locale,
|
|
726
831
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
727
832
|
},
|
|
728
833
|
screen: {
|
|
729
|
-
darkMode:
|
|
730
|
-
dpr:
|
|
834
|
+
darkMode: import_react_native7.Appearance.getColorScheme() === "dark",
|
|
835
|
+
dpr: import_react_native7.PixelRatio.get(),
|
|
731
836
|
height: screen.height,
|
|
732
837
|
width: screen.width,
|
|
733
838
|
orientation: screen.width > screen.height ? "landscape" : "portrait"
|
|
@@ -760,7 +865,7 @@ var getApp = async () => {
|
|
|
760
865
|
};
|
|
761
866
|
var getSdk = async () => ({
|
|
762
867
|
name: "sdk-react-native",
|
|
763
|
-
platform:
|
|
868
|
+
platform: import_react_native7.Platform.OS === "ios" ? "ios" : "android",
|
|
764
869
|
version
|
|
765
870
|
});
|
|
766
871
|
var getTcf = async () => {
|
|
@@ -778,7 +883,38 @@ var getTcf = async () => {
|
|
|
778
883
|
}
|
|
779
884
|
};
|
|
780
885
|
var AdsProvider = (props) => {
|
|
781
|
-
|
|
886
|
+
const [advertisingId, setAdvertisingId] = (0, import_react3.useState)();
|
|
887
|
+
const [vendorId, setVendorId] = (0, import_react3.useState)();
|
|
888
|
+
const initializeIfa = async () => {
|
|
889
|
+
try {
|
|
890
|
+
if (import_react_native7.Platform.OS === "ios") {
|
|
891
|
+
await requestTrackingAuthorization();
|
|
892
|
+
}
|
|
893
|
+
const ids = await resolveIds({
|
|
894
|
+
advertisingId: props.advertisingId ?? void 0,
|
|
895
|
+
vendorId: props.vendorId ?? void 0
|
|
896
|
+
});
|
|
897
|
+
setAdvertisingId(ids.advertisingId);
|
|
898
|
+
setVendorId(ids.vendorId);
|
|
899
|
+
} catch (error) {
|
|
900
|
+
console.error(error);
|
|
901
|
+
}
|
|
902
|
+
};
|
|
903
|
+
(0, import_react3.useEffect)(() => {
|
|
904
|
+
initializeIfa();
|
|
905
|
+
}, []);
|
|
906
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
907
|
+
import_sdk_react2.AdsProviderInternal,
|
|
908
|
+
{
|
|
909
|
+
...props,
|
|
910
|
+
advertisingId,
|
|
911
|
+
vendorId,
|
|
912
|
+
getDevice,
|
|
913
|
+
getSdk,
|
|
914
|
+
getApp,
|
|
915
|
+
getTcf
|
|
916
|
+
}
|
|
917
|
+
);
|
|
782
918
|
};
|
|
783
919
|
// Annotate the CommonJS export names for ESM import in node:
|
|
784
920
|
0 && (module.exports = {
|
package/dist/index.mjs
CHANGED
|
@@ -25,15 +25,17 @@ import {
|
|
|
25
25
|
useBid,
|
|
26
26
|
useIframeUrl
|
|
27
27
|
} from "@kontextso/sdk-react";
|
|
28
|
-
import { useContext, useEffect, useRef, useState } from "react";
|
|
29
|
-
import { Keyboard, Linking, Modal, useWindowDimensions, View } from "react-native";
|
|
28
|
+
import { useContext, useEffect, useRef as useRef2, useState } from "react";
|
|
29
|
+
import { Keyboard, Linking as Linking2, Modal, useWindowDimensions, View } from "react-native";
|
|
30
30
|
|
|
31
31
|
// src/frame-webview.tsx
|
|
32
|
-
import { forwardRef } from "react";
|
|
32
|
+
import { forwardRef, useRef } from "react";
|
|
33
|
+
import { Linking } from "react-native";
|
|
33
34
|
import { WebView } from "react-native-webview";
|
|
34
35
|
import { jsx } from "react/jsx-runtime";
|
|
35
36
|
var FrameWebView = forwardRef(
|
|
36
37
|
({ iframeUrl, onMessage, style, onError, onLoad }, forwardedRef) => {
|
|
38
|
+
const isLoadedRef = useRef(false);
|
|
37
39
|
return /* @__PURE__ */ jsx(
|
|
38
40
|
WebView,
|
|
39
41
|
{
|
|
@@ -58,9 +60,25 @@ var FrameWebView = forwardRef(
|
|
|
58
60
|
window.ReactNativeWebView.postMessage(JSON.stringify(event.data));
|
|
59
61
|
}
|
|
60
62
|
}, false);
|
|
63
|
+
|
|
61
64
|
`,
|
|
62
|
-
onError
|
|
63
|
-
|
|
65
|
+
onError: (event) => onError(
|
|
66
|
+
new Error(`${event.nativeEvent.title}: ${event.nativeEvent.description} [${event.nativeEvent.url}]`),
|
|
67
|
+
true
|
|
68
|
+
),
|
|
69
|
+
onLoad: () => {
|
|
70
|
+
isLoadedRef.current = true;
|
|
71
|
+
onLoad();
|
|
72
|
+
},
|
|
73
|
+
onShouldStartLoadWithRequest: (request) => {
|
|
74
|
+
if (!isLoadedRef.current || request.url.match(/^about:/) || request.url.startsWith(iframeUrl)) {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
Linking.openURL(request.url).catch((err) => {
|
|
78
|
+
onError(new Error(`Failed to open URL: ${request.url} [${err.message}]`));
|
|
79
|
+
});
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
64
82
|
}
|
|
65
83
|
);
|
|
66
84
|
}
|
|
@@ -157,14 +175,14 @@ var Format = ({ code, messageId, wrapper, onEvent, ...otherParams }) => {
|
|
|
157
175
|
const [modalLoaded, setModalLoaded] = useState(false);
|
|
158
176
|
const [containerStyles, setContainerStyles] = useState({});
|
|
159
177
|
const [iframeStyles, setIframeStyles] = useState({});
|
|
160
|
-
const containerRef =
|
|
161
|
-
const webViewRef =
|
|
162
|
-
const modalWebViewRef =
|
|
163
|
-
const messageStatusRef =
|
|
164
|
-
const modalInitTimeoutRef =
|
|
165
|
-
const isModalInitRef =
|
|
178
|
+
const containerRef = useRef2(null);
|
|
179
|
+
const webViewRef = useRef2(null);
|
|
180
|
+
const modalWebViewRef = useRef2(null);
|
|
181
|
+
const messageStatusRef = useRef2("none" /* None */);
|
|
182
|
+
const modalInitTimeoutRef = useRef2(null);
|
|
183
|
+
const isModalInitRef = useRef2(false);
|
|
166
184
|
const { height: windowHeight, width: windowWidth } = useWindowDimensions();
|
|
167
|
-
const keyboardHeightRef =
|
|
185
|
+
const keyboardHeightRef = useRef2(0);
|
|
168
186
|
const isAdViewVisible = showIframe && iframeLoaded;
|
|
169
187
|
const reset = () => {
|
|
170
188
|
setHeight(0);
|
|
@@ -174,7 +192,6 @@ var Format = ({ code, messageId, wrapper, onEvent, ...otherParams }) => {
|
|
|
174
192
|
setIframeLoaded(false);
|
|
175
193
|
resetModal();
|
|
176
194
|
context?.resetAll();
|
|
177
|
-
context?.captureError(new Error("Processing iframe error"));
|
|
178
195
|
};
|
|
179
196
|
const resetModal = () => {
|
|
180
197
|
if (modalInitTimeoutRef.current) {
|
|
@@ -285,7 +302,7 @@ var Format = ({ code, messageId, wrapper, onEvent, ...otherParams }) => {
|
|
|
285
302
|
return;
|
|
286
303
|
}
|
|
287
304
|
try {
|
|
288
|
-
await
|
|
305
|
+
await Linking2.openURL(`${context?.adServerUrl}${message.data.url}`);
|
|
289
306
|
} catch (e) {
|
|
290
307
|
console.error("error opening url", e);
|
|
291
308
|
}
|
|
@@ -420,7 +437,6 @@ var Format = ({ code, messageId, wrapper, onEvent, ...otherParams }) => {
|
|
|
420
437
|
case "error-component-iframe":
|
|
421
438
|
case "error-iframe":
|
|
422
439
|
resetModal();
|
|
423
|
-
context?.captureError(new Error("Processing modal iframe error"));
|
|
424
440
|
break;
|
|
425
441
|
case "open-skoverlay-iframe":
|
|
426
442
|
openSkOverlay(
|
|
@@ -547,9 +563,14 @@ var Format = ({ code, messageId, wrapper, onEvent, ...otherParams }) => {
|
|
|
547
563
|
borderWidth: 0,
|
|
548
564
|
...iframeStyles
|
|
549
565
|
},
|
|
550
|
-
onError: () => {
|
|
551
|
-
debug("iframe-error"
|
|
552
|
-
|
|
566
|
+
onError: (error, shouldReset) => {
|
|
567
|
+
debug("iframe-error", {
|
|
568
|
+
error: error.toString()
|
|
569
|
+
});
|
|
570
|
+
context?.captureError(error);
|
|
571
|
+
if (shouldReset) {
|
|
572
|
+
reset();
|
|
573
|
+
}
|
|
553
574
|
},
|
|
554
575
|
onLoad: () => {
|
|
555
576
|
debug("iframe-load");
|
|
@@ -585,9 +606,14 @@ var Format = ({ code, messageId, wrapper, onEvent, ...otherParams }) => {
|
|
|
585
606
|
width: "100%",
|
|
586
607
|
borderWidth: 0
|
|
587
608
|
},
|
|
588
|
-
onError: () => {
|
|
589
|
-
debug("modal-error"
|
|
590
|
-
|
|
609
|
+
onError: (error, shouldReset) => {
|
|
610
|
+
debug("modal-error", {
|
|
611
|
+
error: error.toString()
|
|
612
|
+
});
|
|
613
|
+
context?.captureError(error);
|
|
614
|
+
if (shouldReset) {
|
|
615
|
+
resetModal();
|
|
616
|
+
}
|
|
591
617
|
},
|
|
592
618
|
onLoad: () => {
|
|
593
619
|
debug("modal-load");
|
|
@@ -630,13 +656,92 @@ import {
|
|
|
630
656
|
log
|
|
631
657
|
} from "@kontextso/sdk-react";
|
|
632
658
|
import { fetch as fetchNetworkInfo, NetInfoStateType } from "@react-native-community/netinfo";
|
|
633
|
-
import { Appearance, Dimensions, PixelRatio, Platform as
|
|
659
|
+
import { Appearance, Dimensions, PixelRatio, Platform as Platform4 } from "react-native";
|
|
634
660
|
import DeviceInfo from "react-native-device-info";
|
|
635
661
|
|
|
636
662
|
// package.json
|
|
637
|
-
var version = "3.
|
|
663
|
+
var version = "3.4.0-rc.0";
|
|
664
|
+
|
|
665
|
+
// src/services/Att.ts
|
|
666
|
+
import { Platform as Platform3 } from "react-native";
|
|
667
|
+
var ZERO_UUID = "00000000-0000-0000-0000-000000000000";
|
|
668
|
+
var didRunStartupFlow = false;
|
|
669
|
+
var startupFlowPromise = null;
|
|
670
|
+
function requestTrackingAuthorization() {
|
|
671
|
+
if (didRunStartupFlow) {
|
|
672
|
+
return startupFlowPromise ?? Promise.resolve();
|
|
673
|
+
}
|
|
674
|
+
didRunStartupFlow = true;
|
|
675
|
+
startupFlowPromise = _runStartupFlow();
|
|
676
|
+
return startupFlowPromise;
|
|
677
|
+
}
|
|
678
|
+
async function getTrackingAuthorizationStatus() {
|
|
679
|
+
if (Platform3.OS !== "ios") {
|
|
680
|
+
return 4 /* NotSupported */;
|
|
681
|
+
}
|
|
682
|
+
try {
|
|
683
|
+
const raw = await NativeRNKontext_default.getTrackingAuthorizationStatus();
|
|
684
|
+
return _mapRawStatus(raw);
|
|
685
|
+
} catch (error) {
|
|
686
|
+
console.error(error);
|
|
687
|
+
return 4 /* NotSupported */;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
async function getAdvertisingId() {
|
|
691
|
+
try {
|
|
692
|
+
const id = await NativeRNKontext_default.getAdvertisingId();
|
|
693
|
+
return _normalizeId(id);
|
|
694
|
+
} catch (error) {
|
|
695
|
+
console.error(error);
|
|
696
|
+
return null;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
async function getVendorId() {
|
|
700
|
+
if (Platform3.OS !== "ios") {
|
|
701
|
+
return null;
|
|
702
|
+
}
|
|
703
|
+
try {
|
|
704
|
+
const id = await NativeRNKontext_default.getVendorId();
|
|
705
|
+
return _normalizeId(id);
|
|
706
|
+
} catch (error) {
|
|
707
|
+
console.error(error);
|
|
708
|
+
return null;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
async function resolveIds(fallbacks) {
|
|
712
|
+
const [vendorId, advertisingId] = await Promise.all([getVendorId(), getAdvertisingId()]);
|
|
713
|
+
return {
|
|
714
|
+
vendorId: vendorId ?? _normalizeId(fallbacks?.vendorId),
|
|
715
|
+
advertisingId: advertisingId ?? _normalizeId(fallbacks?.advertisingId)
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
async function _runStartupFlow() {
|
|
719
|
+
if (Platform3.OS !== "ios") return;
|
|
720
|
+
try {
|
|
721
|
+
if (!_isVersionAtLeast145(Platform3.Version)) return;
|
|
722
|
+
const status = await getTrackingAuthorizationStatus();
|
|
723
|
+
if (status !== 0 /* NotDetermined */) return;
|
|
724
|
+
await NativeRNKontext_default.requestTrackingAuthorization();
|
|
725
|
+
} catch (error) {
|
|
726
|
+
console.error(error);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
function _isVersionAtLeast145(version2) {
|
|
730
|
+
const [major = 0, minor = 0] = version2.split(".").map(Number);
|
|
731
|
+
return major > 14 || major === 14 && minor >= 5;
|
|
732
|
+
}
|
|
733
|
+
function _mapRawStatus(raw) {
|
|
734
|
+
if (raw >= 0 && raw <= 4) return raw;
|
|
735
|
+
return 4 /* NotSupported */;
|
|
736
|
+
}
|
|
737
|
+
function _normalizeId(id) {
|
|
738
|
+
if (!id || id.trim() === "") return null;
|
|
739
|
+
if (id.toLowerCase() === ZERO_UUID) return null;
|
|
740
|
+
return id;
|
|
741
|
+
}
|
|
638
742
|
|
|
639
743
|
// src/context/AdsProvider.tsx
|
|
744
|
+
import { useEffect as useEffect2, useState as useState2 } from "react";
|
|
640
745
|
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
641
746
|
ErrorUtils.setGlobalHandler((error, isFatal) => {
|
|
642
747
|
if (!isFatal) {
|
|
@@ -692,7 +797,7 @@ var getDevice = async () => {
|
|
|
692
797
|
detail: networkInfo.type === NetInfoStateType.cellular && networkInfo.details.cellularGeneration || void 0
|
|
693
798
|
},
|
|
694
799
|
os: {
|
|
695
|
-
name:
|
|
800
|
+
name: Platform4.OS,
|
|
696
801
|
version: DeviceInfo.getSystemVersion(),
|
|
697
802
|
locale: Intl.DateTimeFormat().resolvedOptions().locale,
|
|
698
803
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
@@ -732,7 +837,7 @@ var getApp = async () => {
|
|
|
732
837
|
};
|
|
733
838
|
var getSdk = async () => ({
|
|
734
839
|
name: "sdk-react-native",
|
|
735
|
-
platform:
|
|
840
|
+
platform: Platform4.OS === "ios" ? "ios" : "android",
|
|
736
841
|
version
|
|
737
842
|
});
|
|
738
843
|
var getTcf = async () => {
|
|
@@ -750,7 +855,38 @@ var getTcf = async () => {
|
|
|
750
855
|
}
|
|
751
856
|
};
|
|
752
857
|
var AdsProvider = (props) => {
|
|
753
|
-
|
|
858
|
+
const [advertisingId, setAdvertisingId] = useState2();
|
|
859
|
+
const [vendorId, setVendorId] = useState2();
|
|
860
|
+
const initializeIfa = async () => {
|
|
861
|
+
try {
|
|
862
|
+
if (Platform4.OS === "ios") {
|
|
863
|
+
await requestTrackingAuthorization();
|
|
864
|
+
}
|
|
865
|
+
const ids = await resolveIds({
|
|
866
|
+
advertisingId: props.advertisingId ?? void 0,
|
|
867
|
+
vendorId: props.vendorId ?? void 0
|
|
868
|
+
});
|
|
869
|
+
setAdvertisingId(ids.advertisingId);
|
|
870
|
+
setVendorId(ids.vendorId);
|
|
871
|
+
} catch (error) {
|
|
872
|
+
console.error(error);
|
|
873
|
+
}
|
|
874
|
+
};
|
|
875
|
+
useEffect2(() => {
|
|
876
|
+
initializeIfa();
|
|
877
|
+
}, []);
|
|
878
|
+
return /* @__PURE__ */ jsx4(
|
|
879
|
+
AdsProviderInternal,
|
|
880
|
+
{
|
|
881
|
+
...props,
|
|
882
|
+
advertisingId,
|
|
883
|
+
vendorId,
|
|
884
|
+
getDevice,
|
|
885
|
+
getSdk,
|
|
886
|
+
getApp,
|
|
887
|
+
getTcf
|
|
888
|
+
}
|
|
889
|
+
);
|
|
754
890
|
};
|
|
755
891
|
export {
|
|
756
892
|
AdsProvider,
|
package/ios/KontextSDK.swift
CHANGED
|
@@ -3,6 +3,8 @@ import AVFoundation
|
|
|
3
3
|
import StoreKit
|
|
4
4
|
import UIKit
|
|
5
5
|
import React
|
|
6
|
+
import AdSupport
|
|
7
|
+
import AppTrackingTransparency
|
|
6
8
|
|
|
7
9
|
@objc(KontextSDK)
|
|
8
10
|
public class KontextSDK: NSObject {
|
|
@@ -134,4 +136,49 @@ public class KontextSDK: NSObject {
|
|
|
134
136
|
return out
|
|
135
137
|
}
|
|
136
138
|
|
|
139
|
+
// MARK: - ATT / IFA
|
|
140
|
+
|
|
141
|
+
private static let notSupportedStatus = 4
|
|
142
|
+
|
|
143
|
+
@objc
|
|
144
|
+
public static func getTrackingAuthorizationStatus() -> NSNumber {
|
|
145
|
+
if #available(iOS 14, *) {
|
|
146
|
+
return NSNumber(value: ATTrackingManager.trackingAuthorizationStatus.rawValue)
|
|
147
|
+
}
|
|
148
|
+
return NSNumber(value: notSupportedStatus)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
@objc
|
|
152
|
+
public static func requestTrackingAuthorization(
|
|
153
|
+
_ resolve: @escaping RCTPromiseResolveBlock,
|
|
154
|
+
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
155
|
+
) {
|
|
156
|
+
if #available(iOS 14, *) {
|
|
157
|
+
TrackingAuthorizationManager.shared.requestTrackingAuthorization { statusRaw in
|
|
158
|
+
resolve(NSNumber(value: statusRaw))
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
resolve(NSNumber(value: notSupportedStatus))
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
@objc
|
|
166
|
+
public static func getAdvertisingId() -> String? {
|
|
167
|
+
let manager = ASIdentifierManager.shared()
|
|
168
|
+
if #available(iOS 14.0, *) {
|
|
169
|
+
guard ATTrackingManager.trackingAuthorizationStatus == .authorized else {
|
|
170
|
+
return nil
|
|
171
|
+
}
|
|
172
|
+
return manager.advertisingIdentifier.uuidString
|
|
173
|
+
}
|
|
174
|
+
return manager.isAdvertisingTrackingEnabled
|
|
175
|
+
? manager.advertisingIdentifier.uuidString
|
|
176
|
+
: nil
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
@objc
|
|
180
|
+
public static func getVendorId() -> String? {
|
|
181
|
+
return UIDevice.current.identifierForVendor?.uuidString
|
|
182
|
+
}
|
|
183
|
+
|
|
137
184
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
|
|
6
|
+
<key>NSPrivacyTracking</key><true/>
|
|
7
|
+
|
|
8
|
+
<key>NSPrivacyTrackingDomains</key>
|
|
9
|
+
<array>
|
|
10
|
+
<string>megabrain.co</string>
|
|
11
|
+
</array>
|
|
12
|
+
|
|
13
|
+
<key>NSPrivacyCollectedDataTypes</key>
|
|
14
|
+
<array>
|
|
15
|
+
|
|
16
|
+
<dict>
|
|
17
|
+
<key>NSPrivacyCollectedDataType</key>
|
|
18
|
+
<string>NSPrivacyCollectedDataTypeDeviceID</string>
|
|
19
|
+
<key>NSPrivacyCollectedDataTypeLinked</key><true/>
|
|
20
|
+
<key>NSPrivacyCollectedDataTypeTracking</key><true/>
|
|
21
|
+
<key>NSPrivacyCollectedDataTypePurposes</key>
|
|
22
|
+
<array>
|
|
23
|
+
<string>NSPrivacyCollectedDataTypePurposeThirdPartyAdvertising</string>
|
|
24
|
+
</array>
|
|
25
|
+
</dict>
|
|
26
|
+
|
|
27
|
+
<dict>
|
|
28
|
+
<key>NSPrivacyCollectedDataType</key>
|
|
29
|
+
<string>NSPrivacyCollectedDataTypeOtherAppInfo</string>
|
|
30
|
+
<key>NSPrivacyCollectedDataTypeLinked</key><false/>
|
|
31
|
+
<key>NSPrivacyCollectedDataTypeTracking</key><false/>
|
|
32
|
+
<key>NSPrivacyCollectedDataTypePurposes</key>
|
|
33
|
+
<array>
|
|
34
|
+
<string>NSPrivacyCollectedDataTypePurposeAnalytics</string>
|
|
35
|
+
</array>
|
|
36
|
+
</dict>
|
|
37
|
+
|
|
38
|
+
<dict>
|
|
39
|
+
<key>NSPrivacyCollectedDataType</key>
|
|
40
|
+
<string>NSPrivacyCollectedDataTypeOtherDiagnosticData</string>
|
|
41
|
+
<key>NSPrivacyCollectedDataTypeLinked</key><false/>
|
|
42
|
+
<key>NSPrivacyCollectedDataTypeTracking</key><false/>
|
|
43
|
+
<key>NSPrivacyCollectedDataTypePurposes</key>
|
|
44
|
+
<array>
|
|
45
|
+
<string>NSPrivacyCollectedDataTypePurposeAnalytics</string>
|
|
46
|
+
</array>
|
|
47
|
+
</dict>
|
|
48
|
+
|
|
49
|
+
</array>
|
|
50
|
+
|
|
51
|
+
<key>NSPrivacyAccessedAPITypes</key>
|
|
52
|
+
<array>
|
|
53
|
+
<dict>
|
|
54
|
+
<key>NSPrivacyAccessedAPIType</key>
|
|
55
|
+
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
|
56
|
+
<key>NSPrivacyAccessedAPITypeReasons</key>
|
|
57
|
+
<array>
|
|
58
|
+
<string>CA92.1</string>
|
|
59
|
+
</array>
|
|
60
|
+
</dict>
|
|
61
|
+
</array>
|
|
62
|
+
|
|
63
|
+
</dict>
|
|
64
|
+
</plist>
|
package/ios/RNKontext.mm
CHANGED
|
@@ -56,6 +56,26 @@ RCT_EXPORT_MODULE()
|
|
|
56
56
|
resolve([KontextSDK getTcfData]);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
- (void)getTrackingAuthorizationStatus:(RCTPromiseResolveBlock)resolve
|
|
60
|
+
reject:(RCTPromiseRejectBlock)reject {
|
|
61
|
+
resolve([KontextSDK getTrackingAuthorizationStatus]);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
- (void)requestTrackingAuthorization:(RCTPromiseResolveBlock)resolve
|
|
65
|
+
reject:(RCTPromiseRejectBlock)reject {
|
|
66
|
+
[KontextSDK requestTrackingAuthorization:resolve rejecter:reject];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
- (void)getAdvertisingId:(RCTPromiseResolveBlock)resolve
|
|
70
|
+
reject:(RCTPromiseRejectBlock)reject {
|
|
71
|
+
resolve([KontextSDK getAdvertisingId]);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
- (void)getVendorId:(RCTPromiseResolveBlock)resolve
|
|
75
|
+
reject:(RCTPromiseRejectBlock)reject {
|
|
76
|
+
resolve([KontextSDK getVendorId]);
|
|
77
|
+
}
|
|
78
|
+
|
|
59
79
|
#else
|
|
60
80
|
|
|
61
81
|
RCT_EXPORT_METHOD(isSoundOn : (RCTPromiseResolveBlock)resolve
|
|
@@ -103,6 +123,26 @@ RCT_EXPORT_METHOD(getTcfData:(RCTPromiseResolveBlock)resolve
|
|
|
103
123
|
resolve([KontextSDK getTcfData]);
|
|
104
124
|
}
|
|
105
125
|
|
|
126
|
+
RCT_EXPORT_METHOD(getTrackingAuthorizationStatus:(RCTPromiseResolveBlock)resolve
|
|
127
|
+
rejecter:(RCTPromiseRejectBlock)reject) {
|
|
128
|
+
resolve([KontextSDK getTrackingAuthorizationStatus]);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
RCT_EXPORT_METHOD(requestTrackingAuthorization:(RCTPromiseResolveBlock)resolve
|
|
132
|
+
rejecter:(RCTPromiseRejectBlock)reject) {
|
|
133
|
+
[KontextSDK requestTrackingAuthorization:resolve rejecter:reject];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
RCT_EXPORT_METHOD(getAdvertisingId:(RCTPromiseResolveBlock)resolve
|
|
137
|
+
rejecter:(RCTPromiseRejectBlock)reject) {
|
|
138
|
+
resolve([KontextSDK getAdvertisingId]);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
RCT_EXPORT_METHOD(getVendorId:(RCTPromiseResolveBlock)resolve
|
|
142
|
+
rejecter:(RCTPromiseRejectBlock)reject) {
|
|
143
|
+
resolve([KontextSDK getVendorId]);
|
|
144
|
+
}
|
|
145
|
+
|
|
106
146
|
#endif
|
|
107
147
|
|
|
108
148
|
@end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import AppTrackingTransparency
|
|
3
|
+
import UIKit
|
|
4
|
+
|
|
5
|
+
@available(iOS 14, *)
|
|
6
|
+
final class TrackingAuthorizationManager {
|
|
7
|
+
static let shared = TrackingAuthorizationManager()
|
|
8
|
+
private var observer: NSObjectProtocol?
|
|
9
|
+
|
|
10
|
+
deinit {
|
|
11
|
+
removeObserver()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
func requestTrackingAuthorization(completion: @escaping (Int) -> Void) {
|
|
15
|
+
removeObserver()
|
|
16
|
+
ATTrackingManager.requestTrackingAuthorization { [weak self] status in
|
|
17
|
+
// Race condition: OS returned .denied but system status is still
|
|
18
|
+
// .notDetermined — the dialog couldn't show (app wasn't fully active).
|
|
19
|
+
// Wait for the next foreground activation and retry.
|
|
20
|
+
if status == .denied
|
|
21
|
+
&& ATTrackingManager.trackingAuthorizationStatus == .notDetermined {
|
|
22
|
+
self?.addObserver(completion: completion)
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
completion(Int(status.rawValue))
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private func addObserver(completion: @escaping (Int) -> Void) {
|
|
30
|
+
removeObserver()
|
|
31
|
+
observer = NotificationCenter.default.addObserver(
|
|
32
|
+
forName: UIApplication.didBecomeActiveNotification,
|
|
33
|
+
object: nil,
|
|
34
|
+
queue: .main
|
|
35
|
+
) { [weak self] _ in
|
|
36
|
+
self?.requestTrackingAuthorization(completion: completion)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private func removeObserver() {
|
|
41
|
+
if let observer = observer {
|
|
42
|
+
NotificationCenter.default.removeObserver(observer)
|
|
43
|
+
self.observer = nil
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kontextso/sdk-react-native",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.4.0-rc.0",
|
|
4
4
|
"description": "Kontext SDK for React Native",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"react-native-webview": "^13.10.0"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@kontextso/sdk-react": "^3.0.
|
|
57
|
+
"@kontextso/sdk-react": "^3.0.8-rc.0"
|
|
58
58
|
},
|
|
59
59
|
"files": [
|
|
60
60
|
"dist/*",
|
package/src/NativeRNKontext.ts
CHANGED
|
@@ -3,11 +3,25 @@ import { TurboModuleRegistry } from 'react-native'
|
|
|
3
3
|
|
|
4
4
|
export interface Spec extends TurboModule {
|
|
5
5
|
isSoundOn(): Promise<boolean>
|
|
6
|
+
|
|
7
|
+
// SKOverlay
|
|
6
8
|
presentSKOverlay(appStoreId: string, position: string, dismissible: boolean): Promise<boolean>
|
|
7
9
|
dismissSKOverlay(): Promise<boolean>
|
|
10
|
+
|
|
11
|
+
// SKStoreProduct
|
|
8
12
|
presentSKStoreProduct(appStoreId: string): Promise<boolean>
|
|
9
13
|
dismissSKStoreProduct(): Promise<boolean>
|
|
14
|
+
|
|
15
|
+
// TCF
|
|
10
16
|
getTcfData(): Promise<{ gdprApplies: 0 | 1 | null; tcString: string | null }>
|
|
17
|
+
|
|
18
|
+
// ATT
|
|
19
|
+
getTrackingAuthorizationStatus(): Promise<number>
|
|
20
|
+
requestTrackingAuthorization(): Promise<number>
|
|
21
|
+
|
|
22
|
+
// IFA
|
|
23
|
+
getAdvertisingId(): Promise<string | null>
|
|
24
|
+
getVendorId(): Promise<string | null>
|
|
11
25
|
}
|
|
12
26
|
|
|
13
27
|
export default TurboModuleRegistry.getEnforcing<Spec>('RNKontext')
|
|
@@ -12,6 +12,8 @@ import { Appearance, Dimensions, PixelRatio, Platform } from 'react-native'
|
|
|
12
12
|
import DeviceInfo, { type DeviceType } from 'react-native-device-info'
|
|
13
13
|
import { version } from '../../package.json'
|
|
14
14
|
import NativeRNKontext from '../NativeRNKontext'
|
|
15
|
+
import { requestTrackingAuthorization, resolveIds } from '../services/Att'
|
|
16
|
+
import { useEffect, useState } from 'react'
|
|
15
17
|
|
|
16
18
|
ErrorUtils.setGlobalHandler((error, isFatal) => {
|
|
17
19
|
if (!isFatal) {
|
|
@@ -142,5 +144,35 @@ const getTcf = async (): Promise<Pick<RegulatoryConfig, 'gdpr' | 'gdprConsent'>>
|
|
|
142
144
|
}
|
|
143
145
|
|
|
144
146
|
export const AdsProvider = (props: AdsProviderProps) => {
|
|
145
|
-
|
|
147
|
+
const [advertisingId, setAdvertisingId] = useState<string | null | undefined>()
|
|
148
|
+
const [vendorId, setVendorId] = useState<string | null | undefined>()
|
|
149
|
+
|
|
150
|
+
const initializeIfa = async () => {
|
|
151
|
+
try {
|
|
152
|
+
if (Platform.OS === 'ios') {
|
|
153
|
+
await requestTrackingAuthorization()
|
|
154
|
+
}
|
|
155
|
+
const ids = await resolveIds({
|
|
156
|
+
advertisingId: props.advertisingId ?? undefined,
|
|
157
|
+
vendorId: props.vendorId ?? undefined,
|
|
158
|
+
})
|
|
159
|
+
setAdvertisingId(ids.advertisingId)
|
|
160
|
+
setVendorId(ids.vendorId)
|
|
161
|
+
} catch (error) {
|
|
162
|
+
console.error(error)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
initializeIfa()
|
|
168
|
+
}, [])
|
|
169
|
+
return <AdsProviderInternal
|
|
170
|
+
{...props}
|
|
171
|
+
advertisingId={advertisingId}
|
|
172
|
+
vendorId={vendorId}
|
|
173
|
+
getDevice={getDevice}
|
|
174
|
+
getSdk={getSdk}
|
|
175
|
+
getApp={getApp}
|
|
176
|
+
getTcf={getTcf}
|
|
177
|
+
/>
|
|
146
178
|
}
|
package/src/formats/Format.tsx
CHANGED
|
@@ -18,8 +18,8 @@ import { useContext, useEffect, useRef, useState } from 'react'
|
|
|
18
18
|
import { Keyboard, Linking, Modal, useWindowDimensions, View } from 'react-native'
|
|
19
19
|
import type { WebView, WebViewMessageEvent } from 'react-native-webview'
|
|
20
20
|
import FrameWebView from '../frame-webview'
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
21
|
+
import { dismissSKOverlay, presentSKOverlay, type SKOverlayPosition } from '../services/SkOverlay'
|
|
22
|
+
import { dismissSKStoreProduct, presentSKStoreProduct } from '../services/SkStoreProduct'
|
|
23
23
|
|
|
24
24
|
const sendMessage = (
|
|
25
25
|
webViewRef: React.RefObject<WebView>,
|
|
@@ -97,7 +97,6 @@ const Format = ({ code, messageId, wrapper, onEvent, ...otherParams }: FormatPro
|
|
|
97
97
|
setIframeLoaded(false)
|
|
98
98
|
resetModal()
|
|
99
99
|
context?.resetAll()
|
|
100
|
-
context?.captureError(new Error('Processing iframe error'))
|
|
101
100
|
}
|
|
102
101
|
|
|
103
102
|
const resetModal = () => {
|
|
@@ -380,7 +379,6 @@ const Format = ({ code, messageId, wrapper, onEvent, ...otherParams }: FormatPro
|
|
|
380
379
|
case 'error-component-iframe':
|
|
381
380
|
case 'error-iframe':
|
|
382
381
|
resetModal()
|
|
383
|
-
context?.captureError(new Error('Processing modal iframe error'))
|
|
384
382
|
break
|
|
385
383
|
|
|
386
384
|
case 'open-skoverlay-iframe':
|
|
@@ -532,9 +530,14 @@ const Format = ({ code, messageId, wrapper, onEvent, ...otherParams }: FormatPro
|
|
|
532
530
|
borderWidth: 0,
|
|
533
531
|
...iframeStyles,
|
|
534
532
|
}}
|
|
535
|
-
onError={() => {
|
|
536
|
-
debug('iframe-error'
|
|
537
|
-
|
|
533
|
+
onError={(error, shouldReset) => {
|
|
534
|
+
debug('iframe-error', {
|
|
535
|
+
error: error.toString(),
|
|
536
|
+
})
|
|
537
|
+
context?.captureError(error)
|
|
538
|
+
if (shouldReset) {
|
|
539
|
+
reset()
|
|
540
|
+
}
|
|
538
541
|
}}
|
|
539
542
|
onLoad={() => {
|
|
540
543
|
debug('iframe-load')
|
|
@@ -568,9 +571,14 @@ const Format = ({ code, messageId, wrapper, onEvent, ...otherParams }: FormatPro
|
|
|
568
571
|
width: '100%',
|
|
569
572
|
borderWidth: 0,
|
|
570
573
|
}}
|
|
571
|
-
onError={() => {
|
|
572
|
-
debug('modal-error'
|
|
573
|
-
|
|
574
|
+
onError={(error, shouldReset) => {
|
|
575
|
+
debug('modal-error', {
|
|
576
|
+
error: error.toString(),
|
|
577
|
+
})
|
|
578
|
+
context?.captureError(error)
|
|
579
|
+
if (shouldReset) {
|
|
580
|
+
resetModal()
|
|
581
|
+
}
|
|
574
582
|
}}
|
|
575
583
|
onLoad={() => {
|
|
576
584
|
debug('modal-load')
|
package/src/frame-webview.tsx
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
|
-
import { forwardRef } from 'react'
|
|
2
|
-
import type
|
|
1
|
+
import { forwardRef, useRef } from 'react'
|
|
2
|
+
import { Linking, type StyleProp, type ViewStyle } from 'react-native'
|
|
3
3
|
import { WebView, type WebViewMessageEvent } from 'react-native-webview'
|
|
4
4
|
|
|
5
5
|
interface FrameWebViewProps {
|
|
6
6
|
iframeUrl: string
|
|
7
7
|
onMessage: (event: WebViewMessageEvent) => void
|
|
8
8
|
style: StyleProp<ViewStyle>
|
|
9
|
-
onError: () => void
|
|
9
|
+
onError: (error: Error, shouldReset?: boolean) => void
|
|
10
10
|
onLoad: () => void
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
const FrameWebView = forwardRef<WebView, FrameWebViewProps>(
|
|
14
14
|
({ iframeUrl, onMessage, style, onError, onLoad }, forwardedRef) => {
|
|
15
|
+
const isLoadedRef = useRef(false)
|
|
16
|
+
|
|
15
17
|
return (
|
|
16
18
|
<WebView
|
|
17
19
|
ref={forwardedRef}
|
|
@@ -35,9 +37,28 @@ const FrameWebView = forwardRef<WebView, FrameWebViewProps>(
|
|
|
35
37
|
window.ReactNativeWebView.postMessage(JSON.stringify(event.data));
|
|
36
38
|
}
|
|
37
39
|
}, false);
|
|
40
|
+
|
|
38
41
|
`}
|
|
39
|
-
onError={
|
|
40
|
-
|
|
42
|
+
onError={(event) =>
|
|
43
|
+
onError(
|
|
44
|
+
new Error(`${event.nativeEvent.title}: ${event.nativeEvent.description} [${event.nativeEvent.url}]`),
|
|
45
|
+
true
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
onLoad={() => {
|
|
49
|
+
isLoadedRef.current = true
|
|
50
|
+
onLoad()
|
|
51
|
+
}}
|
|
52
|
+
onShouldStartLoadWithRequest={(request) => {
|
|
53
|
+
if (!isLoadedRef.current || request.url.match(/^about:/) || request.url.startsWith(iframeUrl)) {
|
|
54
|
+
return true
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
Linking.openURL(request.url).catch((err) => {
|
|
58
|
+
onError(new Error(`Failed to open URL: ${request.url} [${err.message}]`))
|
|
59
|
+
})
|
|
60
|
+
return false
|
|
61
|
+
}}
|
|
41
62
|
/>
|
|
42
63
|
)
|
|
43
64
|
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { Platform } from 'react-native'
|
|
2
|
+
import NativeRNKontext from '../NativeRNKontext'
|
|
3
|
+
|
|
4
|
+
const ZERO_UUID = '00000000-0000-0000-0000-000000000000'
|
|
5
|
+
|
|
6
|
+
// Mirrors TrackingStatus enum from Flutter / ATTrackingManager raw values
|
|
7
|
+
export enum TrackingStatus {
|
|
8
|
+
NotDetermined = 0,
|
|
9
|
+
Restricted = 1,
|
|
10
|
+
Denied = 2,
|
|
11
|
+
Authorized = 3,
|
|
12
|
+
NotSupported = 4, // iOS < 14 or non-iOS
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let didRunStartupFlow = false
|
|
16
|
+
let startupFlowPromise: Promise<void> | null = null
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Requests ATT authorization on iOS >= 14.5.
|
|
20
|
+
* On iOS 14.0–14.4 we intentionally skip the request: the dialog is not
|
|
21
|
+
* required to read IDFA on those versions, and asking only to get denied
|
|
22
|
+
* would permanently revoke access.
|
|
23
|
+
* Guaranteed to run only once per app session regardless of call count.
|
|
24
|
+
*/
|
|
25
|
+
export function requestTrackingAuthorization(): Promise<void> {
|
|
26
|
+
if (didRunStartupFlow) {
|
|
27
|
+
return startupFlowPromise ?? Promise.resolve()
|
|
28
|
+
}
|
|
29
|
+
didRunStartupFlow = true
|
|
30
|
+
startupFlowPromise = _runStartupFlow()
|
|
31
|
+
return startupFlowPromise
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function getTrackingAuthorizationStatus(): Promise<TrackingStatus> {
|
|
35
|
+
if (Platform.OS !== 'ios') {
|
|
36
|
+
return TrackingStatus.NotSupported
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const raw = await NativeRNKontext.getTrackingAuthorizationStatus()
|
|
40
|
+
return _mapRawStatus(raw)
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error(error)
|
|
43
|
+
return TrackingStatus.NotSupported
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function getAdvertisingId(): Promise<string | null> {
|
|
48
|
+
try {
|
|
49
|
+
const id = await NativeRNKontext.getAdvertisingId()
|
|
50
|
+
return _normalizeId(id)
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error(error)
|
|
53
|
+
return null
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function getVendorId(): Promise<string | null> {
|
|
58
|
+
if (Platform.OS !== 'ios') {
|
|
59
|
+
return null
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const id = await NativeRNKontext.getVendorId()
|
|
63
|
+
return _normalizeId(id)
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.error(error)
|
|
66
|
+
return null
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function resolveIds(fallbacks?: {
|
|
71
|
+
vendorId?: string | undefined
|
|
72
|
+
advertisingId?: string | undefined
|
|
73
|
+
}): Promise<{ vendorId: string | null; advertisingId: string | null }> {
|
|
74
|
+
const [vendorId, advertisingId] = await Promise.all([getVendorId(), getAdvertisingId()])
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
vendorId: vendorId ?? _normalizeId(fallbacks?.vendorId),
|
|
78
|
+
advertisingId: advertisingId ?? _normalizeId(fallbacks?.advertisingId),
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ─── internal ────────────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
async function _runStartupFlow(): Promise<void> {
|
|
85
|
+
if (Platform.OS !== 'ios') return
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
if (!_isVersionAtLeast145(Platform.Version)) return
|
|
89
|
+
|
|
90
|
+
const status = await getTrackingAuthorizationStatus()
|
|
91
|
+
if (status !== TrackingStatus.NotDetermined) return
|
|
92
|
+
|
|
93
|
+
await NativeRNKontext.requestTrackingAuthorization()
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.error(error)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* iOS 14.0–14.4: ATT dialog not required for IDFA access.
|
|
101
|
+
* Requesting it risks permanent denial, so we only request on >= 14.5.
|
|
102
|
+
*/
|
|
103
|
+
function _isVersionAtLeast145(version: string): boolean {
|
|
104
|
+
const [major = 0, minor = 0] = version.split('.').map(Number)
|
|
105
|
+
return major > 14 || (major === 14 && minor >= 5)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function _mapRawStatus(raw: number): TrackingStatus {
|
|
109
|
+
if (raw >= 0 && raw <= 4) return raw as TrackingStatus
|
|
110
|
+
return TrackingStatus.NotSupported
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function _normalizeId(id: string | null | undefined): string | null {
|
|
114
|
+
if (!id || id.trim() === '') return null
|
|
115
|
+
if (id.toLowerCase() === ZERO_UUID) return null
|
|
116
|
+
return id
|
|
117
|
+
}
|