@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 +1 -1
- package/RNKontext.podspec +21 -0
- package/android/build.gradle +88 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/so/kontext/react/RNKontextModuleImpl.kt +21 -0
- package/android/src/newarch/java/so/kontext/react/RNKontextModule.kt +22 -0
- package/android/src/newarch/java/so/kontext/react/RNKontextPackage.kt +32 -0
- package/android/src/oldarch/java/so/kontext/react/RNKontextModule.kt +25 -0
- package/android/src/oldarch/java/so/kontext/react/RNKontextPacakge.kt +16 -0
- package/dist/index.js +10 -5
- package/dist/index.mjs +7 -2
- package/ios/KontextSDK.swift +26 -0
- package/ios/RNKontext.h +13 -0
- package/ios/RNKontext.mm +36 -0
- package/package.json +26 -5
- package/src/NativeRNKontext.ts +8 -0
- package/src/context/AdsProvider.tsx +58 -0
- package/src/formats/Format.tsx +370 -0
- package/src/formats/InlineAd.tsx +8 -0
- package/src/frame-webview.tsx +43 -0
- package/src/index.ts +5 -0
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/
|
|
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,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
|
|
403
|
+
var import_react_native3 = require("react-native");
|
|
404
404
|
var import_react_native_device_info = __toESM(require("react-native-device-info"));
|
|
405
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
+
}
|
package/ios/RNKontext.h
ADDED
|
@@ -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
|
package/ios/RNKontext.mm
ADDED
|
@@ -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.
|
|
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
|
-
"
|
|
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,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
|