@kontextso/sdk-react-native 3.0.8 → 3.1.0-rc.1

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/dist/index.js CHANGED
@@ -492,7 +492,7 @@ var import_react_native3 = require("react-native");
492
492
  var import_react_native_device_info = __toESM(require("react-native-device-info"));
493
493
 
494
494
  // package.json
495
- var version = "3.0.8";
495
+ var version = "3.1.0-rc.0";
496
496
 
497
497
  // src/NativeRNKontext.ts
498
498
  var import_react_native2 = require("react-native");
package/dist/index.mjs CHANGED
@@ -464,7 +464,7 @@ import { Appearance, Dimensions, PixelRatio, Platform } from "react-native";
464
464
  import DeviceInfo from "react-native-device-info";
465
465
 
466
466
  // package.json
467
- var version = "3.0.8";
467
+ var version = "3.1.0-rc.0";
468
468
 
469
469
  // src/NativeRNKontext.ts
470
470
  import { TurboModuleRegistry } from "react-native";
@@ -1,9 +1,15 @@
1
+ import Foundation
1
2
  import AVFoundation
3
+ import StoreKit
4
+ import UIKit
5
+ import React
2
6
 
3
7
  @objc(KontextSDK)
4
8
  public class KontextSDK: NSObject {
5
9
  private static let MINIMAL_VOLUME_THRESHOLD: Float = 0.0
6
10
 
11
+ // MARK: - Audio
12
+
7
13
  @objc
8
14
  public static func isSoundOn() -> NSNumber? {
9
15
  let session = AVAudioSession.sharedInstance()
@@ -23,4 +29,40 @@ public class KontextSDK: NSObject {
23
29
  return nil
24
30
  }
25
31
  }
32
+
33
+
34
+ // MARK: - SKOverlay
35
+
36
+ @objc
37
+ public static func presentSKOverlay(
38
+ _ appStoreId: String,
39
+ position: String,
40
+ dismissible: Bool,
41
+ resolver resolve: @escaping RCTPromiseResolveBlock,
42
+ rejecter reject: @escaping RCTPromiseRejectBlock
43
+ ) {
44
+ DispatchQueue.main.async {
45
+ do {
46
+ let ok = try SKOverlayManager.shared.present(
47
+ appStoreId: appStoreId,
48
+ position: position,
49
+ dismissible: dismissible
50
+ )
51
+ resolve(ok)
52
+ } catch {
53
+ reject("SK_OVERLAY_ERROR", error.localizedDescription, error)
54
+ }
55
+ }
56
+ }
57
+
58
+ @objc
59
+ public static func dismissSKOverlay(
60
+ _ resolve: @escaping RCTPromiseResolveBlock,
61
+ rejecter reject: @escaping RCTPromiseRejectBlock
62
+ ) {
63
+ DispatchQueue.main.async {
64
+ let ok = SKOverlayManager.shared.dismiss()
65
+ resolve(ok)
66
+ }
67
+ }
26
68
  }
package/ios/RNKontext.h CHANGED
@@ -1,3 +1,4 @@
1
+ #import <React/RCTBridgeModule.h>
1
2
  #import "RNKontext-Swift.h"
2
3
 
3
4
  #ifdef RCT_NEW_ARCH_ENABLED
package/ios/RNKontext.mm CHANGED
@@ -5,6 +5,7 @@
5
5
  RCT_EXPORT_MODULE()
6
6
 
7
7
  #ifdef RCT_NEW_ARCH_ENABLED
8
+
8
9
  - (void)isSoundOn:(RCTPromiseResolveBlock)resolve
9
10
  reject:(RCTPromiseRejectBlock)reject {
10
11
  NSNumber *isSoundOn = [KontextSDK isSoundOn];
@@ -16,13 +17,34 @@ RCT_EXPORT_MODULE()
16
17
  }
17
18
  }
18
19
 
20
+ // NEW: Present SKOverlay
21
+ - (void)presentSKOverlay:(NSString *)appStoreId
22
+ position:(NSString *)position
23
+ dismissible:(BOOL)dismissible
24
+ resolve:(RCTPromiseResolveBlock)resolve
25
+ reject:(RCTPromiseRejectBlock)reject {
26
+ [KontextSDK presentSKOverlay:appStoreId
27
+ position:position
28
+ dismissible:dismissible
29
+ resolver:resolve
30
+ rejecter:reject];
31
+ }
32
+
33
+ // NEW: Dismiss SKOverlay
34
+ - (void)dismissSKOverlay:(RCTPromiseResolveBlock)resolve
35
+ reject:(RCTPromiseRejectBlock)reject {
36
+ [KontextSDK dismissSKOverlay:resolve rejecter:reject];
37
+ }
38
+
19
39
  - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
20
40
  (const facebook::react::ObjCTurboModule::InitParams &)params {
21
41
  return std::make_shared<facebook::react::NativeRNKontextSpecJSI>(params);
22
42
  }
43
+
23
44
  #else
24
- RCT_EXPORT_METHOD(isSoundOn : (RCTPromiseResolveBlock)
25
- resolve rejecter : (RCTPromiseRejectBlock)reject) {
45
+
46
+ RCT_EXPORT_METHOD(isSoundOn : (RCTPromiseResolveBlock)resolve
47
+ rejecter : (RCTPromiseRejectBlock)reject) {
26
48
  NSNumber *isSoundOn = [KontextSDK isSoundOn];
27
49
 
28
50
  if (isSoundOn == nil) {
@@ -31,6 +53,26 @@ RCT_EXPORT_METHOD(isSoundOn : (RCTPromiseResolveBlock)
31
53
  resolve(isSoundOn);
32
54
  }
33
55
  }
56
+
57
+ // NEW: Present SKOverlay
58
+ RCT_EXPORT_METHOD(presentSKOverlay:(NSString *)appStoreId
59
+ position:(NSString *)position
60
+ dismissible:(BOOL)dismissible
61
+ resolver:(RCTPromiseResolveBlock)resolve
62
+ rejecter:(RCTPromiseRejectBlock)reject) {
63
+ [KontextSDK presentSKOverlay:appStoreId
64
+ position:position
65
+ dismissible:dismissible
66
+ resolver:resolve
67
+ rejecter:reject];
68
+ }
69
+
70
+ // NEW: Dismiss SKOverlay
71
+ RCT_EXPORT_METHOD(dismissSKOverlay:(RCTPromiseResolveBlock)resolve
72
+ rejecter:(RCTPromiseRejectBlock)reject) {
73
+ [KontextSDK dismissSKOverlay:resolve rejecter:reject];
74
+ }
75
+
34
76
  #endif
35
77
 
36
78
  @end
@@ -0,0 +1,49 @@
1
+ import Foundation
2
+ import StoreKit
3
+ import UIKit
4
+
5
+ final class SKOverlayManager {
6
+ static let shared = SKOverlayManager()
7
+
8
+ func present(appStoreId: String, position: String, dismissible: Bool) throws -> Bool {
9
+ guard #available(iOS 14.0, *), #available(iOS 13.0, *) else { return false }
10
+
11
+ guard let scene = UIApplication.shared.connectedScenes
12
+ .compactMap({ $0 as? UIWindowScene })
13
+ .first(where: { $0.activationState == .foregroundActive || $0.activationState == .foregroundInactive })
14
+ else {
15
+ throw NSError(domain: "SKOverlay", code: 1, userInfo: [
16
+ NSLocalizedDescriptionKey: "No active UIWindowScene found"
17
+ ])
18
+ }
19
+
20
+ dismiss(in: scene)
21
+
22
+ let pos: SKOverlay.Position = (position.lowercased() == "bottomraised") ? .bottomRaised : .bottom
23
+
24
+ let config = SKOverlay.AppConfiguration(appIdentifier: appStoreId, position: pos)
25
+ config.userDismissible = dismissible
26
+
27
+ let overlay = SKOverlay(configuration: config)
28
+ overlay.present(in: scene)
29
+ return true
30
+ }
31
+
32
+ func dismiss() -> Bool {
33
+ guard #available(iOS 14.0, *), #available(iOS 13.0, *) else { return false }
34
+
35
+ guard let scene = UIApplication.shared.connectedScenes
36
+ .compactMap({ $0 as? UIWindowScene })
37
+ .first(where: { $0.activationState == .foregroundActive || $0.activationState == .foregroundInactive })
38
+ else { return false }
39
+
40
+ dismiss(in: scene)
41
+ return true
42
+ }
43
+
44
+ @available(iOS 14.0, *)
45
+ @available(iOS 13.0, *)
46
+ private func dismiss(in scene: UIWindowScene) {
47
+ SKOverlay.dismiss(in: scene)
48
+ }
49
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kontextso/sdk-react-native",
3
- "version": "3.0.8",
3
+ "version": "3.1.0-rc.1",
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.0",
23
+ "@kontextso/sdk-common": "^1.0.2",
24
24
  "@kontextso/typescript-config": "*",
25
25
  "@react-native-community/netinfo": "11.3.1",
26
26
  "@testing-library/dom": "^10.4.0",
@@ -3,6 +3,8 @@ import { TurboModuleRegistry } from 'react-native'
3
3
 
4
4
  export interface Spec extends TurboModule {
5
5
  isSoundOn(): Promise<boolean>
6
+ presentSKOverlay(appStoreId: string, position: string, dismissible: boolean): Promise<boolean>
7
+ dismissSKOverlay(): Promise<boolean>
6
8
  }
7
9
 
8
- export default TurboModuleRegistry.getEnforcing<Spec>('RNKontext')
10
+ export default TurboModuleRegistry.getEnforcing<Spec>("RNKontext");
@@ -10,7 +10,7 @@ import { fetch as fetchNetworkInfo, NetInfoStateType } from '@react-native-commu
10
10
  import { Appearance, Dimensions, PixelRatio, Platform } from 'react-native'
11
11
  import DeviceInfo, { type DeviceType } from 'react-native-device-info'
12
12
  import { version } from '../../package.json'
13
- import KontextSDK from '../NativeRNKontext'
13
+ import NativeRNKontext from '../NativeRNKontext'
14
14
 
15
15
  ErrorUtils.setGlobalHandler((error, isFatal) => {
16
16
  if (!isFatal) {
@@ -24,7 +24,7 @@ const getDevice = async (): Promise<DeviceConfig> => {
24
24
  try {
25
25
  const powerState = await DeviceInfo.getPowerState()
26
26
  const deviceType = DeviceInfo.getDeviceType() as DeviceType
27
- const soundOn = await KontextSDK.isSoundOn()
27
+ const soundOn = await NativeRNKontext.isSoundOn()
28
28
  const screen = Dimensions.get('screen')
29
29
  const networkInfo = await fetchNetworkInfo()
30
30
 
@@ -18,10 +18,11 @@ 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 NativeRNKontext from '../NativeRNKontext'
21
22
 
22
23
  const sendMessage = (
23
24
  webViewRef: React.RefObject<WebView>,
24
- type: Extract<IframeMessageType, 'update-iframe' | 'update-dimensions-iframe'>,
25
+ type: Extract<IframeMessageType, 'update-iframe' | 'update-dimensions-iframe' | 'update-skoverlay-iframe'>,
25
26
  code: string,
26
27
  data: any
27
28
  ) => {
@@ -90,6 +91,7 @@ const Format = ({ code, messageId, wrapper, onEvent, ...otherParams }: FormatPro
90
91
  setContainerStyles({})
91
92
  setIframeStyles({})
92
93
  setIframeLoaded(false)
94
+ closeSkOverlay()
93
95
  resetModal()
94
96
  context?.resetAll()
95
97
  context?.captureError(new Error('Processing iframe error'))
@@ -137,6 +139,29 @@ const Format = ({ code, messageId, wrapper, onEvent, ...otherParams }: FormatPro
137
139
  })
138
140
  }
139
141
 
142
+ const openSkOverlay = async (appStoreId: string, position: string, dismissible: boolean) => {
143
+ try {
144
+ await NativeRNKontext.presentSKOverlay(appStoreId, position, dismissible)
145
+ sendMessage(webViewRef, 'update-skoverlay-iframe', code, {
146
+ open: true
147
+ })
148
+ } catch (e) {
149
+ console.error('error opening sk overlay', e)
150
+ reset()
151
+ }
152
+ }
153
+
154
+ const closeSkOverlay = async () => {
155
+ try {
156
+ await NativeRNKontext.dismissSKOverlay()
157
+ sendMessage(webViewRef, 'update-skoverlay-iframe', code, {
158
+ open: false
159
+ })
160
+ } catch (e) {
161
+ console.error('error dismissing sk overlay', e)
162
+ }
163
+ }
164
+
140
165
  debug('format-update-state')
141
166
 
142
167
  const onMessage = (event: WebViewMessageEvent) => {
@@ -217,6 +242,14 @@ const Format = ({ code, messageId, wrapper, onEvent, ...otherParams }: FormatPro
217
242
  context?.onAdEventInternal(message.data)
218
243
  messageStatusRef.current = MessageStatus.MessageReceived
219
244
  break
245
+
246
+ case 'open-skoverlay-iframe':
247
+ openSkOverlay(message.data.appStoreId, message.data.position, message.data.dismissible)
248
+ break
249
+
250
+ case 'close-skoverlay-iframe':
251
+ closeSkOverlay()
252
+ break
220
253
  }
221
254
  },
222
255
  {
@@ -0,0 +1,42 @@
1
+ import { Platform } from 'react-native'
2
+ import NativeRNKontext from '../NativeRNKontext';
3
+
4
+ export type SKOverlayPosition = 'bottom' | 'bottomRaised';
5
+
6
+ const isValidAppStoreId = (id: unknown): id is string => {
7
+ return typeof id === "string" && /^\d+$/.test(id);
8
+ }
9
+
10
+ const isValidPosition = (p: unknown): p is SKOverlayPosition => {
11
+ return p === "bottom" || p === "bottomRaised";
12
+ }
13
+
14
+ export async function presentSKOverlay(params: {
15
+ appStoreId: string;
16
+ position: SKOverlayPosition;
17
+ dismissible: boolean;
18
+ }) {
19
+ if (Platform.OS !== 'ios') {
20
+ return false
21
+ }
22
+ let { appStoreId, position, dismissible } = params
23
+
24
+ if (!isValidAppStoreId(appStoreId)) {
25
+ return false
26
+ }
27
+ if (!isValidPosition(position)) {
28
+ position = 'bottom'
29
+ }
30
+ if (typeof dismissible !== 'boolean') {
31
+ dismissible = Boolean(dismissible)
32
+ }
33
+
34
+ return NativeRNKontext.presentSKOverlay(appStoreId, position, dismissible)
35
+ }
36
+
37
+ export async function dismissSKOverlay() {
38
+ if (Platform.OS !== 'ios') {
39
+ return false
40
+ }
41
+ return NativeRNKontext.dismissSKOverlay()
42
+ }