@kontextso/sdk-react-native 3.3.1-rc.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 +118 -8
- package/dist/index.mjs +115 -5
- 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 +3 -3
- package/src/NativeRNKontext.ts +14 -0
- package/src/context/AdsProvider.tsx +33 -1
- 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
|
@@ -684,13 +684,92 @@ var InlineAd_default = InlineAd;
|
|
|
684
684
|
// src/context/AdsProvider.tsx
|
|
685
685
|
var import_sdk_react2 = require("@kontextso/sdk-react");
|
|
686
686
|
var import_netinfo = require("@react-native-community/netinfo");
|
|
687
|
-
var
|
|
687
|
+
var import_react_native7 = require("react-native");
|
|
688
688
|
var import_react_native_device_info = __toESM(require("react-native-device-info"));
|
|
689
689
|
|
|
690
690
|
// package.json
|
|
691
|
-
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
|
+
}
|
|
692
770
|
|
|
693
771
|
// src/context/AdsProvider.tsx
|
|
772
|
+
var import_react3 = require("react");
|
|
694
773
|
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
695
774
|
ErrorUtils.setGlobalHandler((error, isFatal) => {
|
|
696
775
|
if (!isFatal) {
|
|
@@ -709,7 +788,7 @@ var getDevice = async () => {
|
|
|
709
788
|
const powerState = await import_react_native_device_info.default.getPowerState();
|
|
710
789
|
const deviceType = import_react_native_device_info.default.getDeviceType();
|
|
711
790
|
const soundOn = await NativeRNKontext_default.isSoundOn();
|
|
712
|
-
const screen =
|
|
791
|
+
const screen = import_react_native7.Dimensions.get("screen");
|
|
713
792
|
const networkInfo = await (0, import_netinfo.fetch)();
|
|
714
793
|
const mapDeviceTypeToHardwareType = () => {
|
|
715
794
|
switch (deviceType) {
|
|
@@ -746,14 +825,14 @@ var getDevice = async () => {
|
|
|
746
825
|
detail: networkInfo.type === import_netinfo.NetInfoStateType.cellular && networkInfo.details.cellularGeneration || void 0
|
|
747
826
|
},
|
|
748
827
|
os: {
|
|
749
|
-
name:
|
|
828
|
+
name: import_react_native7.Platform.OS,
|
|
750
829
|
version: import_react_native_device_info.default.getSystemVersion(),
|
|
751
830
|
locale: Intl.DateTimeFormat().resolvedOptions().locale,
|
|
752
831
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
753
832
|
},
|
|
754
833
|
screen: {
|
|
755
|
-
darkMode:
|
|
756
|
-
dpr:
|
|
834
|
+
darkMode: import_react_native7.Appearance.getColorScheme() === "dark",
|
|
835
|
+
dpr: import_react_native7.PixelRatio.get(),
|
|
757
836
|
height: screen.height,
|
|
758
837
|
width: screen.width,
|
|
759
838
|
orientation: screen.width > screen.height ? "landscape" : "portrait"
|
|
@@ -786,7 +865,7 @@ var getApp = async () => {
|
|
|
786
865
|
};
|
|
787
866
|
var getSdk = async () => ({
|
|
788
867
|
name: "sdk-react-native",
|
|
789
|
-
platform:
|
|
868
|
+
platform: import_react_native7.Platform.OS === "ios" ? "ios" : "android",
|
|
790
869
|
version
|
|
791
870
|
});
|
|
792
871
|
var getTcf = async () => {
|
|
@@ -804,7 +883,38 @@ var getTcf = async () => {
|
|
|
804
883
|
}
|
|
805
884
|
};
|
|
806
885
|
var AdsProvider = (props) => {
|
|
807
|
-
|
|
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
|
+
);
|
|
808
918
|
};
|
|
809
919
|
// Annotate the CommonJS export names for ESM import in node:
|
|
810
920
|
0 && (module.exports = {
|
package/dist/index.mjs
CHANGED
|
@@ -656,13 +656,92 @@ import {
|
|
|
656
656
|
log
|
|
657
657
|
} from "@kontextso/sdk-react";
|
|
658
658
|
import { fetch as fetchNetworkInfo, NetInfoStateType } from "@react-native-community/netinfo";
|
|
659
|
-
import { Appearance, Dimensions, PixelRatio, Platform as
|
|
659
|
+
import { Appearance, Dimensions, PixelRatio, Platform as Platform4 } from "react-native";
|
|
660
660
|
import DeviceInfo from "react-native-device-info";
|
|
661
661
|
|
|
662
662
|
// package.json
|
|
663
|
-
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
|
+
}
|
|
664
742
|
|
|
665
743
|
// src/context/AdsProvider.tsx
|
|
744
|
+
import { useEffect as useEffect2, useState as useState2 } from "react";
|
|
666
745
|
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
667
746
|
ErrorUtils.setGlobalHandler((error, isFatal) => {
|
|
668
747
|
if (!isFatal) {
|
|
@@ -718,7 +797,7 @@ var getDevice = async () => {
|
|
|
718
797
|
detail: networkInfo.type === NetInfoStateType.cellular && networkInfo.details.cellularGeneration || void 0
|
|
719
798
|
},
|
|
720
799
|
os: {
|
|
721
|
-
name:
|
|
800
|
+
name: Platform4.OS,
|
|
722
801
|
version: DeviceInfo.getSystemVersion(),
|
|
723
802
|
locale: Intl.DateTimeFormat().resolvedOptions().locale,
|
|
724
803
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
@@ -758,7 +837,7 @@ var getApp = async () => {
|
|
|
758
837
|
};
|
|
759
838
|
var getSdk = async () => ({
|
|
760
839
|
name: "sdk-react-native",
|
|
761
|
-
platform:
|
|
840
|
+
platform: Platform4.OS === "ios" ? "ios" : "android",
|
|
762
841
|
version
|
|
763
842
|
});
|
|
764
843
|
var getTcf = async () => {
|
|
@@ -776,7 +855,38 @@ var getTcf = async () => {
|
|
|
776
855
|
}
|
|
777
856
|
};
|
|
778
857
|
var AdsProvider = (props) => {
|
|
779
|
-
|
|
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
|
+
);
|
|
780
890
|
};
|
|
781
891
|
export {
|
|
782
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",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"format": "biome format --write ."
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@kontextso/sdk-common": "^1.0.
|
|
23
|
+
"@kontextso/sdk-common": "^1.0.4",
|
|
24
24
|
"@kontextso/typescript-config": "*",
|
|
25
25
|
"@react-native-community/netinfo": "11.3.1",
|
|
26
26
|
"@testing-library/dom": "^10.4.0",
|
|
@@ -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
|
}
|
|
@@ -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
|
+
}
|