@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.
@@ -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,2 +1,3 @@
1
1
  <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
- </manifest>
2
+ <uses-permission android:name="com.google.android.gms.permission.AD_ID" />
3
+ </manifest>
@@ -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 import_react_native6 = require("react-native");
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.3.1-rc.0";
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 = import_react_native6.Dimensions.get("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: import_react_native6.Platform.OS,
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: import_react_native6.Appearance.getColorScheme() === "dark",
756
- dpr: import_react_native6.PixelRatio.get(),
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: import_react_native6.Platform.OS === "ios" ? "ios" : "android",
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
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_sdk_react2.AdsProviderInternal, { ...props, getDevice, getSdk, getApp, getTcf });
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 Platform3 } from "react-native";
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.3.1-rc.0";
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: Platform3.OS,
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: Platform3.OS === "ios" ? "ios" : "android",
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
- return /* @__PURE__ */ jsx4(AdsProviderInternal, { ...props, getDevice, getSdk, getApp, getTcf });
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,
@@ -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.1-rc.0",
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.5",
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.7"
57
+ "@kontextso/sdk-react": "^3.0.8-rc.0"
58
58
  },
59
59
  "files": [
60
60
  "dist/*",
@@ -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
- return <AdsProviderInternal {...props} getDevice={getDevice} getSdk={getSdk} getApp={getApp} getTcf={getTcf} />
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
+ }