@kontextso/sdk-react-native 2.1.1-rc.0 → 2.2.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/README.md CHANGED
@@ -83,5 +83,5 @@ function MessageList({ messages }: { messages: Message[] }) {
83
83
 
84
84
  ## Additional Resources
85
85
 
86
- * Explore a working [Demo Project featuring SDK integration](https://github.com/kontextso/kontext-react-native-demo).
86
+ * Explore a working [Demo Project featuring SDK integration](https://github.com/kontextso/sdk-react-native-demo).
87
87
  * Full documentation: [Kontext React Native SDK](https://docs.kontext.so/sdk/react-native).
@@ -0,0 +1,21 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = "RNKontext"
7
+ s.version = package["version"]
8
+ s.summary = package["description"]
9
+ s.homepage = package["homepage"]
10
+ s.license = package["license"]
11
+ s.authors = package["author"]
12
+
13
+ s.platforms = { :ios => min_ios_version_supported }
14
+ s.source = { :git => ".git", :tag => "#{s.version}" }
15
+
16
+ s.source_files = "ios/**/*.{h,m,mm,cpp,swift}"
17
+ s.private_header_files = "ios/**/*.h"
18
+
19
+
20
+ install_modules_dependencies(s)
21
+ end
@@ -0,0 +1,88 @@
1
+ buildscript {
2
+ ext.getExtOrDefault = {name ->
3
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['RNKontext_' + name]
4
+ }
5
+
6
+ repositories {
7
+ google()
8
+ mavenCentral()
9
+ }
10
+
11
+ dependencies {
12
+ classpath "com.android.tools.build:gradle:8.7.2"
13
+ // noinspection DifferentKotlinGradleVersion
14
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
15
+ }
16
+ }
17
+
18
+
19
+ apply plugin: "com.android.library"
20
+ apply plugin: "kotlin-android"
21
+
22
+ apply plugin: "com.facebook.react"
23
+
24
+ def isNewArchitectureEnabled() {
25
+ return project.hasProperty("newArchEnabled") && project.newArchEnabled == "true"
26
+ }
27
+
28
+ def getExtOrIntegerDefault(name) {
29
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["RNKontext_" + name]).toInteger()
30
+ }
31
+
32
+ android {
33
+ namespace "so.kontext.react"
34
+
35
+ compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
36
+
37
+ defaultConfig {
38
+ minSdkVersion getExtOrIntegerDefault("minSdkVersion")
39
+ targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
40
+ // Expose the new-arch flag to runtime and build-time Kotlin/Java code
41
+ buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
42
+ }
43
+
44
+ buildFeatures {
45
+ buildConfig true
46
+ }
47
+
48
+ buildTypes {
49
+ release {
50
+ minifyEnabled false
51
+ }
52
+ }
53
+
54
+ lintOptions {
55
+ disable "GradleCompatible"
56
+ }
57
+
58
+ compileOptions {
59
+ sourceCompatibility JavaVersion.VERSION_1_8
60
+ targetCompatibility JavaVersion.VERSION_1_8
61
+ }
62
+
63
+ sourceSets {
64
+ main {
65
+ java.srcDirs += [
66
+ "generated/java",
67
+ "generated/jni"
68
+ ]
69
+ if (isNewArchitectureEnabled()) {
70
+ java.srcDirs += ['src/newarch/java']
71
+ } else {
72
+ java.srcDirs += ['src/oldarch/java']
73
+ }
74
+ }
75
+ }
76
+ }
77
+
78
+ repositories {
79
+ mavenCentral()
80
+ google()
81
+ }
82
+
83
+ def kotlin_version = getExtOrDefault("kotlinVersion")
84
+
85
+ dependencies {
86
+ implementation "com.facebook.react:react-android"
87
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
88
+ }
@@ -0,0 +1,5 @@
1
+ RNKontext_kotlinVersion=2.0.21
2
+ RNKontext_minSdkVersion=24
3
+ RNKontext_targetSdkVersion=34
4
+ RNKontext_compileSdkVersion=35
5
+ RNKontext_ndkVersion=27.1.12297006
@@ -0,0 +1,2 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ </manifest>
@@ -0,0 +1,21 @@
1
+ package so.kontext.react
2
+
3
+ import com.facebook.react.bridge.Promise
4
+ import com.facebook.react.bridge.ReactApplicationContext
5
+ import android.content.Context
6
+ import android.media.AudioManager
7
+
8
+ class RNKontextModuleImpl(private val reactContext: ReactApplicationContext) {
9
+ fun isSoundOn(promise: Promise?) {
10
+ val audioManager = reactContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
11
+ val current = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
12
+ val max = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
13
+ val volume = if (max > 0) current.toFloat() / max.toFloat() else 0f
14
+
15
+ promise?.resolve(volume > MINIMAL_VOLUME_THRESHOLD)
16
+ }
17
+
18
+ companion object {
19
+ private const val MINIMAL_VOLUME_THRESHOLD = 0.0f
20
+ }
21
+ }
@@ -0,0 +1,22 @@
1
+ package so.kontext.react
2
+
3
+ import com.facebook.react.bridge.ReactApplicationContext
4
+ import com.facebook.react.module.annotations.ReactModule
5
+ import com.facebook.react.bridge.Promise
6
+
7
+ @ReactModule(name = RNKontextModule.NAME)
8
+ class RNKontextModule(reactContext: ReactApplicationContext) :
9
+ NativeRNKontextSpec(reactContext) {
10
+
11
+ private val impl = RNKontextModuleImpl(reactContext)
12
+
13
+ override fun getName(): String = NAME
14
+
15
+ override fun isSoundOn(promise: Promise?) {
16
+ impl.isSoundOn(promise)
17
+ }
18
+
19
+ companion object {
20
+ const val NAME = "RNKontext"
21
+ }
22
+ }
@@ -0,0 +1,32 @@
1
+ package so.kontext.react
2
+
3
+ import com.facebook.react.BaseReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.module.model.ReactModuleInfo
7
+ import com.facebook.react.module.model.ReactModuleInfoProvider
8
+
9
+ class RNKontextPackage : BaseReactPackage() {
10
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
11
+ return if (name == RNKontextModule.NAME) {
12
+ RNKontextModule(reactContext)
13
+ } else {
14
+ null
15
+ }
16
+ }
17
+
18
+ override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
19
+ return ReactModuleInfoProvider {
20
+ mapOf(
21
+ RNKontextModule.NAME to ReactModuleInfo(
22
+ RNKontextModule.NAME,
23
+ RNKontextModule.NAME,
24
+ false, // canOverrideExistingModule
25
+ false, // needsEagerInit
26
+ false, // isCxxModule
27
+ true // isTurboModule
28
+ )
29
+ )
30
+ }
31
+ }
32
+ }
@@ -0,0 +1,25 @@
1
+ package so.kontext.react
2
+
3
+ import com.facebook.react.bridge.ReactApplicationContext
4
+ import com.facebook.react.bridge.ReactContextBaseJavaModule
5
+ import com.facebook.react.bridge.ReactMethod
6
+ import com.facebook.react.bridge.Promise
7
+ import android.content.Context
8
+ import android.media.AudioManager
9
+
10
+ class RNKontextModule(reactContext: ReactApplicationContext) :
11
+ ReactContextBaseJavaModule(reactContext) {
12
+
13
+ private val impl = RNKontextModuleImpl(reactContext)
14
+
15
+ override fun getName(): String = NAME
16
+
17
+ @ReactMethod
18
+ fun isSoundOn(promise: Promise?) {
19
+ impl.isSoundOn(promise)
20
+ }
21
+
22
+ companion object {
23
+ const val NAME = "RNKontext"
24
+ }
25
+ }
@@ -0,0 +1,16 @@
1
+ package so.kontext.react
2
+
3
+ import com.facebook.react.ReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.uimanager.ViewManager
7
+
8
+ class RNKontextPackage : ReactPackage {
9
+ override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
10
+ return listOf(RNKontextModule(reactContext))
11
+ }
12
+
13
+ override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
14
+ return emptyList()
15
+ }
16
+ }
package/dist/index.js CHANGED
@@ -400,9 +400,14 @@ var InlineAd_default = InlineAd;
400
400
 
401
401
  // src/context/AdsProvider.tsx
402
402
  var import_sdk_react2 = require("@kontextso/sdk-react");
403
- var import_react_native2 = require("react-native");
403
+ var import_react_native3 = require("react-native");
404
404
  var import_react_native_device_info = __toESM(require("react-native-device-info"));
405
- var import_soundon = require("@kontextso/soundon");
405
+
406
+ // src/NativeRNKontext.ts
407
+ var import_react_native2 = require("react-native");
408
+ var NativeRNKontext_default = import_react_native2.TurboModuleRegistry.getEnforcing("RNKontext");
409
+
410
+ // src/context/AdsProvider.tsx
406
411
  var import_jsx_runtime4 = require("react/jsx-runtime");
407
412
  ErrorUtils.setGlobalHandler((error, isFatal) => {
408
413
  if (!isFatal) {
@@ -413,7 +418,7 @@ ErrorUtils.setGlobalHandler((error, isFatal) => {
413
418
  });
414
419
  var getDevice = async () => {
415
420
  try {
416
- const os = import_react_native2.Platform.OS;
421
+ const os = import_react_native3.Platform.OS;
417
422
  const systemVersion = import_react_native_device_info.default.getSystemVersion();
418
423
  const model = import_react_native_device_info.default.getModel();
419
424
  const brand = import_react_native_device_info.default.getBrand();
@@ -423,11 +428,11 @@ var getDevice = async () => {
423
428
  const appVersion = import_react_native_device_info.default.getVersion();
424
429
  let soundOn = false;
425
430
  try {
426
- soundOn = await (0, import_soundon.isSoundOn)();
431
+ soundOn = await NativeRNKontext_default.isSoundOn();
427
432
  } catch (error) {
428
433
  import_sdk_react2.log.warn("Failed to read output volume", error);
429
434
  }
430
- const rnv = import_react_native2.Platform.constants.reactNativeVersion;
435
+ const rnv = import_react_native3.Platform.constants.reactNativeVersion;
431
436
  const reactNativeVersion = `${rnv.major}.${rnv.minor}.${rnv.patch}`;
432
437
  return {
433
438
  os,
package/dist/index.mjs CHANGED
@@ -371,7 +371,12 @@ var InlineAd_default = InlineAd;
371
371
  import { AdsProviderInternal, log } from "@kontextso/sdk-react";
372
372
  import { Platform } from "react-native";
373
373
  import DeviceInfo from "react-native-device-info";
374
- import { isSoundOn } from "@kontextso/soundon";
374
+
375
+ // src/NativeRNKontext.ts
376
+ import { TurboModuleRegistry } from "react-native";
377
+ var NativeRNKontext_default = TurboModuleRegistry.getEnforcing("RNKontext");
378
+
379
+ // src/context/AdsProvider.tsx
375
380
  import { jsx as jsx4 } from "react/jsx-runtime";
376
381
  ErrorUtils.setGlobalHandler((error, isFatal) => {
377
382
  if (!isFatal) {
@@ -392,7 +397,7 @@ var getDevice = async () => {
392
397
  const appVersion = DeviceInfo.getVersion();
393
398
  let soundOn = false;
394
399
  try {
395
- soundOn = await isSoundOn();
400
+ soundOn = await NativeRNKontext_default.isSoundOn();
396
401
  } catch (error) {
397
402
  log.warn("Failed to read output volume", error);
398
403
  }
@@ -0,0 +1,26 @@
1
+ import AVFoundation
2
+
3
+ @objc(KontextSDK)
4
+ public class KontextSDK: NSObject {
5
+ private static let MINIMAL_VOLUME_THRESHOLD: Float = 0.0
6
+
7
+ @objc
8
+ public static func isSoundOn() -> NSNumber? {
9
+ let session = AVAudioSession.sharedInstance()
10
+
11
+ do {
12
+ try session.setCategory(
13
+ .ambient,
14
+ mode: .default,
15
+ options: [.mixWithOthers]
16
+ )
17
+ try session.setActive(true)
18
+
19
+ return NSNumber(
20
+ value: session.outputVolume > MINIMAL_VOLUME_THRESHOLD
21
+ )
22
+ } catch {
23
+ return nil
24
+ }
25
+ }
26
+ }
@@ -0,0 +1,13 @@
1
+ #import "RNKontext-Swift.h"
2
+
3
+ #ifdef RCT_NEW_ARCH_ENABLED
4
+ #import <RNKontextSpec/RNKontextSpec.h>
5
+
6
+ @interface RNKontext: NSObject <NativeRNKontextSpec>
7
+ #else
8
+ #import <React/RCTBridgeModule.h>
9
+
10
+ @interface RNKontext: NSObject <RCTBridgeModule>
11
+ #endif
12
+
13
+ @end
@@ -0,0 +1,36 @@
1
+ #import "RNKontext.h"
2
+
3
+ @implementation RNKontext
4
+
5
+ RCT_EXPORT_MODULE()
6
+
7
+ #ifdef RCT_NEW_ARCH_ENABLED
8
+ - (void)isSoundOn:(RCTPromiseResolveBlock)resolve
9
+ reject:(RCTPromiseRejectBlock)reject {
10
+ NSNumber *isSoundOn = [KontextSDK isSoundOn];
11
+
12
+ if (isSoundOn == nil) {
13
+ reject(@"soundon_error", @"Failed to read output volume", nil);
14
+ } else {
15
+ resolve(isSoundOn);
16
+ }
17
+ }
18
+
19
+ - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
20
+ (const facebook::react::ObjCTurboModule::InitParams &)params {
21
+ return std::make_shared<facebook::react::NativeRNKontextSpecJSI>(params);
22
+ }
23
+ #else
24
+ RCT_EXPORT_METHOD(isSoundOn : (RCTPromiseResolveBlock)
25
+ resolve rejecter : (RCTPromiseRejectBlock)reject) {
26
+ NSNumber *isSoundOn = [KontextSDK isSoundOn];
27
+
28
+ if (isSoundOn == nil) {
29
+ reject(@"soundon_error", @"Failed to read output volume", nil);
30
+ } else {
31
+ resolve(isSoundOn);
32
+ }
33
+ }
34
+ #endif
35
+
36
+ @end
package/package.json CHANGED
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "name": "@kontextso/sdk-react-native",
3
- "version": "2.1.1-rc.0",
3
+ "version": "2.2.0-rc.1",
4
+ "description": "Kontext SDK for React Native",
4
5
  "main": "./dist/index.js",
5
6
  "module": "./dist/index.mjs",
6
7
  "types": "./dist/index.d.ts",
7
8
  "type": "commonjs",
8
9
  "license": "Apache-2.0",
10
+ "author": "Kontext",
11
+ "homepage": "https://github.com/kontextso",
9
12
  "scripts": {
10
13
  "dev:js": "tsup --watch",
11
14
  "dev": "npm-run-all dev:js",
@@ -52,11 +55,29 @@
52
55
  "react-native-webview": "^13.15.0"
53
56
  },
54
57
  "dependencies": {
55
- "@kontextso/sdk-react": "^1.2.4",
56
- "@kontextso/soundon": "^1.0.0"
58
+ "@kontextso/sdk-react": "^1.2.4"
57
59
  },
58
60
  "files": [
59
61
  "dist/*",
60
- "LICENSE"
61
- ]
62
+ "src",
63
+ "android",
64
+ "ios",
65
+ "*.podspec",
66
+ "LICENSE",
67
+ "!ios/build",
68
+ "!android/build",
69
+ "!android/gradle",
70
+ "!android/gradlew",
71
+ "!android/gradlew.bat",
72
+ "!android/local.properties",
73
+ "!**/.*"
74
+ ],
75
+ "codegenConfig": {
76
+ "name": "RNKontextSpec",
77
+ "type": "modules",
78
+ "jsSrcsDir": "src",
79
+ "android": {
80
+ "javaPackageName": "so.kontext.react"
81
+ }
82
+ }
62
83
  }
@@ -0,0 +1,8 @@
1
+ import type { TurboModule } from 'react-native'
2
+ import { TurboModuleRegistry } from 'react-native'
3
+
4
+ export interface Spec extends TurboModule {
5
+ isSoundOn(): Promise<boolean>
6
+ }
7
+
8
+ export default TurboModuleRegistry.getEnforcing<Spec>('RNKontext')
@@ -0,0 +1,58 @@
1
+ 'use client'
2
+
3
+ import type { AdsProviderProps, Device } from '@kontextso/sdk-react'
4
+ import { AdsProviderInternal, log } from '@kontextso/sdk-react'
5
+ import { Platform } from 'react-native'
6
+ import DeviceInfo from 'react-native-device-info'
7
+ import KontextSDK from '../NativeRNKontext'
8
+
9
+ ErrorUtils.setGlobalHandler((error, isFatal) => {
10
+ if (!isFatal) {
11
+ log.warn(error)
12
+ } else {
13
+ log.error(error)
14
+ }
15
+ })
16
+
17
+ const getDevice = async (): Promise<Device> => {
18
+ try {
19
+ const os = Platform.OS
20
+ const systemVersion = DeviceInfo.getSystemVersion()
21
+ const model = DeviceInfo.getModel()
22
+ const brand = DeviceInfo.getBrand()
23
+ const deviceId = DeviceInfo.getDeviceId()
24
+ const deviceType = DeviceInfo.getDeviceType()
25
+ const appBundleId = DeviceInfo.getBundleId()
26
+ const appVersion = DeviceInfo.getVersion()
27
+
28
+ let soundOn = false
29
+ try {
30
+ soundOn = await KontextSDK.isSoundOn()
31
+ } catch (error) {
32
+ log.warn('Failed to read output volume', error)
33
+ }
34
+
35
+ const rnv = Platform.constants.reactNativeVersion
36
+ const reactNativeVersion = `${rnv.major}.${rnv.minor}.${rnv.patch}`
37
+
38
+ return {
39
+ os,
40
+ systemVersion,
41
+ reactNativeVersion,
42
+ model,
43
+ brand,
44
+ deviceId,
45
+ deviceType,
46
+ appBundleId,
47
+ appVersion,
48
+ soundOn,
49
+ }
50
+ } catch (error) {
51
+ console.error(error)
52
+ }
53
+ return {}
54
+ }
55
+
56
+ export const AdsProvider = (props: AdsProviderProps) => {
57
+ return <AdsProviderInternal {...props} getDevice={getDevice} />
58
+ }
@@ -0,0 +1,370 @@
1
+ import { useContext, useEffect, useRef, useState } from 'react'
2
+ import {
3
+ AdsContext,
4
+ convertParamsToString,
5
+ ErrorBoundary,
6
+ useBid,
7
+ useIframeUrl,
8
+ type FormatProps,
9
+ } from '@kontextso/sdk-react'
10
+ import type { WebView, WebViewMessageEvent } from 'react-native-webview'
11
+ import { Linking, Modal, View, useWindowDimensions } from 'react-native'
12
+ import {
13
+ handleIframeMessage,
14
+ type IframeMessageEvent,
15
+ type IframeMessageType,
16
+ makeIframeMessage,
17
+ type IframeMessage,
18
+ } from '@kontextso/sdk-common'
19
+ import FrameWebView from '../frame-webview'
20
+
21
+ const sendMessage = (
22
+ webViewRef: React.RefObject<WebView>,
23
+ type: Extract<IframeMessageType, 'update-iframe' | 'update-dimensions-iframe'>,
24
+ code: string,
25
+ data: any
26
+ ) => {
27
+ const message = makeIframeMessage(type, {
28
+ data,
29
+ code,
30
+ })
31
+
32
+ webViewRef.current?.injectJavaScript(`
33
+ window.dispatchEvent(new MessageEvent('message', {
34
+ data: ${JSON.stringify(message)}
35
+ }));
36
+ `)
37
+ }
38
+
39
+ const Format = ({ code, messageId, wrapper, ...otherParams }: FormatProps) => {
40
+ const context = useContext(AdsContext)
41
+
42
+ const bid = useBid({ code, messageId })
43
+ const [height, setHeight] = useState<number>(0)
44
+
45
+ const iframeUrl = useIframeUrl(context, bid, code, messageId, 'sdk-react-native', otherParams.theme)
46
+ const modalUrl = iframeUrl.replace('/api/frame/', '/api/modal/')
47
+
48
+ const [showIframe, setShowIframe] = useState<boolean>(false)
49
+ const [iframeLoaded, setIframeLoaded] = useState<boolean>(false)
50
+
51
+ const [modalOpen, setModalOpen] = useState<boolean>(false)
52
+ const [modalShown, setModalShown] = useState<boolean>(false)
53
+ const [modalLoaded, setModalLoaded] = useState<boolean>(false)
54
+
55
+ const [containerStyles, setContainerStyles] = useState<any>({})
56
+ const [iframeStyles, setIframeStyles] = useState<any>({})
57
+
58
+ const containerRef = useRef<View>(null)
59
+ const webViewRef = useRef<WebView>(null)
60
+ const modalWebViewRef = useRef<WebView>(null)
61
+ const modalInitTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
62
+
63
+ const { height: windowHeight, width: windowWidth } = useWindowDimensions()
64
+
65
+ const reset = () => {
66
+ setHeight(0)
67
+ setShowIframe(false)
68
+ setContainerStyles({})
69
+ setIframeStyles({})
70
+ setIframeLoaded(false)
71
+ resetModal()
72
+ context?.resetAll()
73
+ context?.captureError(new Error('Processing iframe error'))
74
+ }
75
+
76
+ const resetModal = () => {
77
+ if (modalInitTimeoutRef.current) {
78
+ clearTimeout(modalInitTimeoutRef.current)
79
+ modalInitTimeoutRef.current = null
80
+ }
81
+
82
+ setModalOpen(false)
83
+ setModalLoaded(false)
84
+ setModalShown(false)
85
+ }
86
+
87
+ const debug = (name: string, data: any = {}) => {
88
+ context?.onDebugEventInternal?.(name, {
89
+ code,
90
+ messageId,
91
+ otherParams,
92
+ bid,
93
+ iframeUrl,
94
+ iframeLoaded,
95
+ showIframe,
96
+ height,
97
+ containerStyles,
98
+ iframeStyles,
99
+ ...data,
100
+ })
101
+ }
102
+
103
+ const debugModal = (name: string, data: any = {}) => {
104
+ context?.onDebugEventInternal?.(name, {
105
+ code,
106
+ messageId,
107
+ otherParams,
108
+ bid,
109
+ modalUrl,
110
+ modalOpen,
111
+ modalShown,
112
+ modalLoaded,
113
+ ...data,
114
+ })
115
+ }
116
+
117
+ debug('format-update-state')
118
+
119
+ const onMessage = (event: WebViewMessageEvent) => {
120
+ try {
121
+ const data = JSON.parse(event.nativeEvent.data) as IframeMessage
122
+
123
+ debug('iframe-message', {
124
+ message: data,
125
+ })
126
+
127
+ const messageHandler = handleIframeMessage(
128
+ (message) => {
129
+ switch (message.type) {
130
+ case 'init-iframe':
131
+ setIframeLoaded(true)
132
+ debug('iframe-post-message')
133
+ sendMessage(webViewRef, 'update-iframe', code, {
134
+ messages: context?.messages,
135
+ sdk: 'sdk-react-native',
136
+ otherParams,
137
+ messageId,
138
+ })
139
+ break
140
+
141
+ case 'error-iframe':
142
+ reset()
143
+ break
144
+
145
+ case 'resize-iframe':
146
+ setHeight(message.data.height)
147
+ break
148
+
149
+ case 'click-iframe':
150
+ if (message.data.url) {
151
+ Linking.openURL(`${context?.adServerUrl}${message.data.url}`).catch((err) =>
152
+ console.error('error opening url', err)
153
+ )
154
+ }
155
+ context?.onAdClickInternal(message.data)
156
+ break
157
+
158
+ case 'view-iframe':
159
+ context?.onAdViewInternal(message.data)
160
+ break
161
+
162
+ case 'show-iframe':
163
+ setShowIframe(true)
164
+ break
165
+
166
+ case 'hide-iframe':
167
+ setShowIframe(false)
168
+ break
169
+
170
+ case 'set-styles-iframe':
171
+ setContainerStyles(message.data.containerStyles)
172
+ setIframeStyles(message.data.iframeStyles)
173
+ break
174
+
175
+ case 'open-component-iframe':
176
+ setModalOpen(true)
177
+
178
+ modalInitTimeoutRef.current = setTimeout(resetModal, message.data.timeout ?? 5000)
179
+ break
180
+ }
181
+ },
182
+ {
183
+ code,
184
+ }
185
+ )
186
+ messageHandler({ data } as IframeMessageEvent)
187
+ } catch (e) {
188
+ debug('iframe-message-error', {
189
+ error: e,
190
+ })
191
+ console.error('error parsing message from webview', e)
192
+ reset()
193
+ }
194
+ }
195
+
196
+ const onModalMessage = (event: WebViewMessageEvent) => {
197
+ try {
198
+ const data = JSON.parse(event.nativeEvent.data) as IframeMessage
199
+
200
+ debugModal('modal-iframe-message', {
201
+ message: data,
202
+ })
203
+
204
+ const messageHandler = handleIframeMessage(
205
+ (message) => {
206
+ switch (message.type) {
207
+ case 'close-component-iframe':
208
+ resetModal()
209
+ break
210
+
211
+ case 'init-component-iframe':
212
+ if (modalInitTimeoutRef.current) {
213
+ clearTimeout(modalInitTimeoutRef.current)
214
+ modalInitTimeoutRef.current = null
215
+ }
216
+
217
+ setModalShown(true)
218
+ break
219
+
220
+ case 'error-component-iframe':
221
+ case 'error-iframe':
222
+ resetModal()
223
+ break
224
+
225
+ case 'click-iframe':
226
+ if (message.data.url) {
227
+ Linking.openURL(`${context?.adServerUrl}${message.data.url}`).catch((err) =>
228
+ console.error('error opening url', err)
229
+ )
230
+ }
231
+ context?.onAdClickInternal(message.data)
232
+ break
233
+ }
234
+ },
235
+ {
236
+ code,
237
+ component: 'modal',
238
+ }
239
+ )
240
+ messageHandler({ data } as IframeMessageEvent)
241
+ } catch (e) {
242
+ debugModal('modal-iframe-message-error', {
243
+ error: e,
244
+ })
245
+ console.error('error parsing message from webview', e)
246
+ resetModal()
247
+ }
248
+ }
249
+
250
+ const paramsString = convertParamsToString(otherParams)
251
+
252
+ useEffect(() => {
253
+ if (!iframeLoaded || !context?.adServerUrl || !bid || !webViewRef.current) {
254
+ return
255
+ }
256
+ debug('iframe-post-message')
257
+ sendMessage(webViewRef, 'update-iframe', code, {
258
+ data: { otherParams },
259
+ code,
260
+ })
261
+ // because we use the rest params, the object is alaways new and useEffect would be called on every render
262
+ }, [paramsString, iframeLoaded, context?.adServerUrl, bid, code])
263
+
264
+ const checkIfInViewport = () => {
265
+ if (!containerRef.current) return
266
+
267
+ containerRef.current.measureInWindow((containerX, containerY, containerWidth, containerHeight) => {
268
+ sendMessage(webViewRef, 'update-dimensions-iframe', code, {
269
+ windowWidth,
270
+ windowHeight,
271
+ containerWidth,
272
+ containerHeight,
273
+ containerX,
274
+ containerY,
275
+ })
276
+ })
277
+ }
278
+
279
+ useEffect(() => {
280
+ const interval = setInterval(() => {
281
+ checkIfInViewport()
282
+ }, 250)
283
+
284
+ return () => clearInterval(interval)
285
+ }, [])
286
+
287
+ if (!context || !bid || !iframeUrl) {
288
+ return null
289
+ }
290
+
291
+ const getWidth = () => {
292
+ if (showIframe && iframeLoaded) {
293
+ return '100%'
294
+ }
295
+ return 0
296
+ }
297
+
298
+ const getHeight = () => {
299
+ if (showIframe && iframeLoaded) {
300
+ return height
301
+ }
302
+ return 0
303
+ }
304
+
305
+ const content = (
306
+ <>
307
+ <Modal visible={modalOpen} transparent={true} onRequestClose={resetModal}>
308
+ <View
309
+ style={{
310
+ flex: 1,
311
+ // Don't show the modal until the modal page is loaded and sends 'init-component-iframe' message back to SDK
312
+ ...(modalShown ? { opacity: 1, pointerEvents: 'auto' } : { opacity: 0, pointerEvents: 'none' }),
313
+ }}
314
+ >
315
+ <FrameWebView
316
+ ref={modalWebViewRef}
317
+ iframeUrl={modalUrl}
318
+ onMessage={onModalMessage}
319
+ style={{
320
+ backgroundColor: 'transparent',
321
+ height: '100%',
322
+ width: '100%',
323
+ borderWidth: 0,
324
+ }}
325
+ onError={() => {
326
+ debug('modal-error')
327
+ resetModal()
328
+ }}
329
+ onLoad={() => {
330
+ debug('modal-load')
331
+ setModalLoaded(true)
332
+ }}
333
+ />
334
+ </View>
335
+ </Modal>
336
+
337
+ <View style={containerStyles} ref={containerRef}>
338
+ <FrameWebView
339
+ ref={webViewRef}
340
+ iframeUrl={iframeUrl}
341
+ onMessage={onMessage}
342
+ style={{
343
+ height: getHeight(),
344
+ width: getWidth(),
345
+ background: 'transparent',
346
+ borderWidth: 0,
347
+ ...iframeStyles,
348
+ }}
349
+ onError={() => {
350
+ debug('iframe-error')
351
+ reset()
352
+ }}
353
+ onLoad={() => {
354
+ debug('iframe-load')
355
+ }}
356
+ />
357
+ </View>
358
+ </>
359
+ )
360
+
361
+ return wrapper ? wrapper(content) : content
362
+ }
363
+
364
+ const FormatWithErrorBoundary = (props: FormatProps) => (
365
+ <ErrorBoundary>
366
+ <Format {...props} />
367
+ </ErrorBoundary>
368
+ )
369
+
370
+ export default FormatWithErrorBoundary
@@ -0,0 +1,8 @@
1
+ import Format from './Format'
2
+ import { type FormatProps } from '@kontextso/sdk-react'
3
+
4
+ const InlineAd = ({ code, messageId, wrapper, ...props }: FormatProps) => {
5
+ return <Format code={code} messageId={messageId} wrapper={wrapper} {...props} />
6
+ }
7
+
8
+ export default InlineAd
@@ -0,0 +1,43 @@
1
+ import { forwardRef } from 'react'
2
+ import type { StyleProp, ViewStyle } from 'react-native'
3
+ import { WebView, type WebViewMessageEvent } from 'react-native-webview'
4
+
5
+ interface FrameWebViewProps {
6
+ iframeUrl: string
7
+ onMessage: (event: WebViewMessageEvent) => void
8
+ style: StyleProp<ViewStyle>
9
+ onError: () => void
10
+ onLoad: () => void
11
+ }
12
+
13
+ const FrameWebView = forwardRef<WebView, FrameWebViewProps>(
14
+ ({ iframeUrl, onMessage, style, onError, onLoad }, forwardedRef) => {
15
+ return (
16
+ <WebView
17
+ ref={forwardedRef}
18
+ source={{
19
+ uri: iframeUrl,
20
+ }}
21
+ onMessage={onMessage}
22
+ style={style}
23
+ allowsInlineMediaPlayback={true}
24
+ mediaPlaybackRequiresUserAction={false}
25
+ javaScriptEnabled={true}
26
+ domStorageEnabled={true}
27
+ allowsFullscreenVideo={false}
28
+ injectedJavaScript={`
29
+ window.addEventListener("message", function(event) {
30
+ if (window.ReactNativeWebView && event.data) {
31
+ // ReactNativeWebView.postMessage only supports string data
32
+ window.ReactNativeWebView.postMessage(JSON.stringify(event.data));
33
+ }
34
+ }, false);
35
+ `}
36
+ onError={onError}
37
+ onLoad={onLoad}
38
+ />
39
+ )
40
+ }
41
+ )
42
+
43
+ export default FrameWebView
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ import InlineAd from './formats/InlineAd'
2
+
3
+ export * from './context/AdsProvider'
4
+
5
+ export { InlineAd }