@kontextso/sdk-react-native 3.0.7-rc.2 → 4.0.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kontextso/sdk-react-native",
3
- "version": "3.0.7-rc.2",
3
+ "version": "4.0.0-rc.0",
4
4
  "description": "Kontext SDK for React Native",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -10,19 +10,18 @@
10
10
  "author": "Kontext",
11
11
  "homepage": "https://github.com/kontextso",
12
12
  "scripts": {
13
- "dev:js": "tsup --watch",
13
+ "dev:js": "tsup src/index.ts --format cjs,esm --dts --external react react-dom --watch",
14
14
  "dev": "npm-run-all2 dev:js",
15
- "build:js": "tsup",
16
- "build": "npm run build:js && cross-env NODE_ENV=development npm run test:run",
15
+ "build:js": "tsup src/index.ts --format cjs,esm --dts --external react",
16
+ "build": "npm run build:js && cross-env NODE_ENV=development",
17
17
  "test:run": "vitest --run",
18
18
  "test": "vitest --run",
19
- "test:watch": "vitest --watch",
19
+ "test:watch": "vitest --watch",
20
20
  "format": "biome format --write ."
21
21
  },
22
22
  "devDependencies": {
23
23
  "@kontextso/sdk-common": "^1.0.0",
24
24
  "@kontextso/typescript-config": "*",
25
- "@react-native-community/netinfo": "11.3.1",
26
25
  "@testing-library/dom": "^10.4.0",
27
26
  "@testing-library/jest-dom": "^6.4.6",
28
27
  "@testing-library/react": "^16.0.0",
@@ -39,7 +38,7 @@
39
38
  "react": "^18.3.1",
40
39
  "react-dom": "^18.3.1",
41
40
  "react-native": "^0.80.1",
42
- "react-native-device-info": "10.14.0",
41
+ "react-native-device-info": ">=12.0.0",
43
42
  "react-native-webview": "^13.15.0",
44
43
  "react-test-renderer": "^18.3.1",
45
44
  "tsup": "^8.0.2",
@@ -47,14 +46,13 @@
47
46
  "vitest": "^2.1.2"
48
47
  },
49
48
  "peerDependencies": {
50
- "@react-native-community/netinfo": "^11.0",
51
49
  "react": ">=18.0.0",
52
50
  "react-native": ">=0.73.0",
53
51
  "react-native-device-info": ">=10.0.0 <15.0.0",
54
52
  "react-native-webview": "^13.10.0"
55
53
  },
56
54
  "dependencies": {
57
- "@kontextso/sdk-react": "^3.0.7-rc.1"
55
+ "@kontextso/sdk-utils": "^1.0.0"
58
56
  },
59
57
  "files": [
60
58
  "dist/*",
@@ -0,0 +1,141 @@
1
+ import { handleIframeMessage, type IframeMessage, makeIframeMessage } from '@kontextso/sdk-common'
2
+ import type { Session } from './Session'
3
+
4
+ export abstract class AbstractStream {
5
+ protected session: Session
6
+ protected element: HTMLIFrameElement | null = null
7
+
8
+ constructor(session: Session) {
9
+ this.session = session
10
+ }
11
+
12
+ public getIframeUrl() {
13
+ const bid = this.session.getLastBid()
14
+ if (!bid) {
15
+ return null
16
+ }
17
+ const messageId = bid.messageId
18
+ const bidId = bid.bidId
19
+
20
+ const params = new URLSearchParams({
21
+ code: 'inlineAd',
22
+ messageId,
23
+ sdk: 'sdk-js',
24
+ })
25
+ const adServerUrl = this.session.config.adServerUrl
26
+ return `${adServerUrl}/api/frame/${bidId}?${params}`
27
+ }
28
+
29
+ protected updateHeight(height: number) {
30
+ if (this.element) {
31
+ this.element.style.height = `${height}px`
32
+ }
33
+ }
34
+
35
+ protected updateDisplay(display: string) {
36
+ if (this.element) {
37
+ this.element.style.display = display
38
+ }
39
+ }
40
+
41
+ protected sendUpdateMessage() {
42
+ const bid = this.session.getLastBid()
43
+ if (!bid) {
44
+ return
45
+ }
46
+ this.element?.contentWindow?.postMessage(
47
+ makeIframeMessage('update-iframe', {
48
+ code: 'inlineAd',
49
+ data: {
50
+ messages: this.session.getMessages(),
51
+ sdk: 'sdk-js',
52
+ messageId: bid.messageId,
53
+ otherParams: {},
54
+ },
55
+ }),
56
+ '*'
57
+ )
58
+ }
59
+
60
+ protected processInitIframeMessage() {
61
+ this.updateDisplay('block')
62
+ this.sendUpdateMessage()
63
+ }
64
+
65
+ protected processErrorIframeMessage(message: IframeMessage<'error-iframe'>) {
66
+ console.log('error-iframe', message.data)
67
+ }
68
+
69
+ protected processResizeIframeMessage(message: IframeMessage<'resize-iframe'>) {
70
+ this.updateHeight(message.data.height)
71
+ }
72
+
73
+ protected processEventIframeMessage(message: IframeMessage<'event-iframe'>) {
74
+ console.log('event-iframe', message.data)
75
+ }
76
+
77
+ protected processShowIframeMessage() {
78
+ this.updateDisplay('block')
79
+ }
80
+
81
+ protected processHideIframeMessage() {
82
+ this.updateDisplay('none')
83
+ }
84
+
85
+ protected setupMessageListener() {
86
+ const { adServerUrl, placementCode } = this.session.config
87
+ const messageHandler = handleIframeMessage(
88
+ (message) => {
89
+ switch (message.type) {
90
+ case 'init-iframe':
91
+ this.processInitIframeMessage()
92
+ break
93
+
94
+ case 'error-iframe':
95
+ this.processErrorIframeMessage(message)
96
+ break
97
+
98
+ case 'resize-iframe':
99
+ this.processResizeIframeMessage(message)
100
+ break
101
+
102
+ case 'event-iframe':
103
+ this.processEventIframeMessage(message)
104
+ break
105
+
106
+ case 'show-iframe':
107
+ this.processShowIframeMessage()
108
+ break
109
+
110
+ case 'hide-iframe':
111
+ this.processHideIframeMessage()
112
+ break
113
+ }
114
+ },
115
+ {
116
+ origin: adServerUrl,
117
+ code: placementCode,
118
+ }
119
+ )
120
+ // TODO: remove listener on unmount
121
+ window.addEventListener('message', messageHandler)
122
+ }
123
+
124
+ protected createIframe() {
125
+ const iframeUrl = this.getIframeUrl()
126
+ if (!iframeUrl) {
127
+ return
128
+ }
129
+ const iframe = document.createElement('iframe')
130
+ iframe.src = iframeUrl
131
+ iframe.title = 'ad-iframe'
132
+ iframe.dataset.testid = 'ad-iframe'
133
+ iframe.style.display = 'none'
134
+ iframe.style.height = '100%'
135
+ iframe.style.width = '100%'
136
+ iframe.style.background = 'transparent'
137
+ iframe.style.border = '0'
138
+ iframe.style.colorScheme = 'auto'
139
+ this.element = iframe
140
+ }
141
+ }
@@ -0,0 +1,129 @@
1
+ const DEFAULT_AD_SERVER_URL = 'https://server.megabrain.co'
2
+ const DEFAULT_PLACEMENT_CODE = 'inlineAd'
3
+ const DEFAULT_LOG_LEVEL = 'info'
4
+
5
+ export type Regulatory = {
6
+ /** Flag that indicates whether or not the request is subject to GDPR regulations 0 = No, 1 = Yes, omission indicates Unknown */
7
+ gdpr?: 0 | 1
8
+
9
+ /** When GDPR regulations are in effect this attribute contains the Transparency and Consent Framework's Consent String data structure */
10
+ gdprConsent?: string
11
+
12
+ /** Flag indicating if this request is subject to the COPPA regulations established by the USA FTC, where 0 = no, 1 = yes, omission indicates Unknown */
13
+ coppa?: 0 | 1
14
+
15
+ /** Contains the Global Privacy Platform's consent string. See IAB-GPP spec for more details */
16
+ gpp?: string
17
+
18
+ /** List of the section(s) of the GPP string which should be applied for this transaction */
19
+ gppSid?: number[]
20
+
21
+ /** Communicates signals regarding consumer privacy under US privacy regulation under CCPA and LSPA */
22
+ usPrivacy?: string
23
+ }
24
+
25
+ export interface Character {
26
+ /** Unique ID of the character */
27
+ id: string
28
+
29
+ /** Name of the character */
30
+ name: string
31
+
32
+ /** URL of the character’s avatar */
33
+ avatarUrl?: string
34
+
35
+ /** Greeting of the character */
36
+ greeting?: string
37
+
38
+ /** Description of the character’s personality */
39
+ persona?: string
40
+
41
+ /** Tags of the character */
42
+ tags?: string[]
43
+
44
+ /** Whether the character is NSFW */
45
+ isNsfw?: boolean
46
+
47
+ [key: string]: string | string[] | boolean | undefined
48
+ }
49
+
50
+ export interface ConfigOptions {
51
+ /** Your publisher token. This token is not secret and is visible in the browser’s developer console. */
52
+ publisherToken: string
53
+
54
+ /** The SDK can be temporarily disabled by setting it to false (no ads will be rendered). */
55
+ userId: string
56
+
57
+ /** Unique identifier of the conversation. */
58
+ conversationId: string
59
+
60
+ /** Character information. */
61
+ character?: Character
62
+ /** A list of allowed placement codes. */
63
+
64
+ placementCode?: string
65
+ /** A variant ID that helps determine which type of ad to render. */
66
+
67
+ variantId?: string
68
+ /** Device-specific identifier provided by the operating systems (IDFA/GAID) */
69
+
70
+ /** Regulatory features. */
71
+ regulatory?: Regulatory
72
+
73
+ /** Local and remote logging settings. */
74
+ logLevel?: 'debug' | 'info' | 'log' | 'warn' | 'error' | 'silent'
75
+
76
+ /** The email of the user. */
77
+ userEmail?: string
78
+
79
+ /** We can set a different server URL (usually used for testing). */
80
+ adServerUrl?: string
81
+ }
82
+
83
+ export class Configuration {
84
+ private config: ConfigOptions
85
+
86
+ constructor(config: ConfigOptions) {
87
+ this.config = config
88
+ }
89
+
90
+ get publisherToken () {
91
+ return this.config.publisherToken
92
+ }
93
+
94
+ get userId () {
95
+ return this.config.userId
96
+ }
97
+
98
+ get conversationId () {
99
+ return this.config.conversationId
100
+ }
101
+
102
+ get character () {
103
+ return this.config.character
104
+ }
105
+
106
+ get placementCode () {
107
+ return this.config.placementCode ?? DEFAULT_PLACEMENT_CODE
108
+ }
109
+
110
+ get variantId () {
111
+ return this.config.variantId
112
+ }
113
+
114
+ get regulatory () {
115
+ return this.config.regulatory
116
+ }
117
+
118
+ get logLevel () {
119
+ return this.config.logLevel ?? DEFAULT_LOG_LEVEL
120
+ }
121
+
122
+ get userEmail () {
123
+ return this.config.userEmail
124
+ }
125
+
126
+ get adServerUrl () {
127
+ return this.config.adServerUrl ?? DEFAULT_AD_SERVER_URL
128
+ }
129
+ }
@@ -0,0 +1,168 @@
1
+ import {
2
+ handleIframeMessage,
3
+ type IframeMessage,
4
+ type IframeMessageEvent,
5
+ type IframeMessageType,
6
+ makeIframeMessage,
7
+ } from '@kontextso/sdk-common'
8
+ import { Session } from './Session'
9
+ import { useRef, useEffect, useState, useCallback } from 'react'
10
+ import { View } from 'react-native'
11
+ import { WebView, type WebViewMessageEvent } from 'react-native-webview'
12
+
13
+ interface InlineAdProps {
14
+ messageId: string
15
+ session: Session
16
+ onDebugEvent: (name: string, data?: any) => void
17
+ }
18
+
19
+ const getIframeUrl = (bidId: string, messageId: string, adServerUrl: string) => {
20
+ const params = new URLSearchParams({
21
+ code: 'inlineAd',
22
+ messageId,
23
+ sdk: 'sdk-react-native',
24
+ })
25
+ return `${adServerUrl}/api/frame/${bidId}?${params}`
26
+ }
27
+
28
+ export const InlineAd = ({ messageId, session, onDebugEvent }: InlineAdProps) => {
29
+ const [bidId, setBidId] = useState<string | null>(null)
30
+ const webViewRef = useRef<WebView>(null)
31
+ const postRef = useRef<{
32
+ [messageId: string]: boolean
33
+ }>({})
34
+
35
+ const sendMessage = (
36
+ webViewRef: React.RefObject<WebView>,
37
+ type: Extract<IframeMessageType, 'update-iframe' | 'update-dimensions-iframe'>,
38
+ code: string,
39
+ data: any
40
+ ) => {
41
+ if (postRef.current[messageId]) {
42
+ onDebugEvent('InlineAd: message already posted', { messageId, ref: postRef.current })
43
+ return
44
+ }
45
+ const message = makeIframeMessage(type, {
46
+ data,
47
+ code,
48
+ })
49
+ onDebugEvent('InlineAd: post message', { messageId, message })
50
+ webViewRef.current?.injectJavaScript(`
51
+ window.dispatchEvent(new MessageEvent('message', {
52
+ data: ${JSON.stringify(message)}
53
+ }));
54
+ `)
55
+ postRef.current[messageId] = true
56
+ }
57
+
58
+ const updateBid = useCallback(() => {
59
+ const bid = session.getLastBid()
60
+ if (!bid || bid.messageId !== messageId) {
61
+ setBidId(null)
62
+ onDebugEvent('InlineAd: no bid', { messageId, bid })
63
+ return
64
+ }
65
+ setBidId(bid.bidId)
66
+ onDebugEvent('InlineAd: update bid', { messageId, bid })
67
+ }, [session, messageId])
68
+
69
+ useEffect(() => {
70
+ session.setOnUpdateBids(updateBid)
71
+ return () => {
72
+ session.setOnUpdateBids(undefined as any)
73
+ onDebugEvent('InlineAd: remove update bids callback', { messageId })
74
+ }
75
+ }, [session, updateBid])
76
+
77
+ // still okay to force an initial update when messageId changes
78
+ useEffect(() => {
79
+ updateBid()
80
+ }, [updateBid])
81
+
82
+ if (!bidId) {
83
+ onDebugEvent('InlineAd: no bid id', { messageId, bidId })
84
+ return null
85
+ }
86
+
87
+ const iframeUrl = getIframeUrl(bidId, messageId, session.config.adServerUrl)
88
+ if (!iframeUrl) {
89
+ onDebugEvent('InlineAd: no iframe url', { messageId, bidId })
90
+ return null
91
+ }
92
+
93
+ const onMessage = (event: WebViewMessageEvent) => {
94
+ try {
95
+ const data = JSON.parse(event.nativeEvent.data) as IframeMessage
96
+ const messageHandler = handleIframeMessage(
97
+ (message) => {
98
+ onDebugEvent('InlineAd: message handler', { messageId, message, bidId })
99
+ switch (message.type) {
100
+ case 'init-iframe':
101
+ sendMessage(webViewRef, 'update-iframe', 'inlineAd', {
102
+ messages: session.getMessages(),
103
+ sdk: 'sdk-react-native',
104
+ otherParams: {},
105
+ messageId,
106
+ })
107
+ break
108
+
109
+ case 'error-iframe':
110
+ break
111
+
112
+ case 'click-iframe':
113
+ break
114
+
115
+ case 'view-iframe':
116
+ break
117
+
118
+ case 'event-iframe':
119
+ break
120
+ }
121
+ },
122
+ {
123
+ code: 'inlineAd',
124
+ }
125
+ )
126
+ messageHandler({ data } as IframeMessageEvent)
127
+ } catch (e) {
128
+ console.error('error parsing message from webview', e)
129
+ onDebugEvent('InlineAd: error parsing message from webview', { messageId, error: e })
130
+ }
131
+ }
132
+
133
+ onDebugEvent('InlineAd: iframe url', { messageId, iframeUrl })
134
+
135
+ return (
136
+ <WebView
137
+ source={{
138
+ uri: iframeUrl,
139
+ }}
140
+ style={{
141
+ height: 300,
142
+ width: '100%',
143
+ backgroundColor: 'red',
144
+ }}
145
+ ref={webViewRef}
146
+ onMessage={onMessage}
147
+ allowsInlineMediaPlayback={true}
148
+ mediaPlaybackRequiresUserAction={false}
149
+ javaScriptEnabled={true}
150
+ domStorageEnabled={true}
151
+ allowsFullscreenVideo={false}
152
+ injectedJavaScript={`
153
+ window.addEventListener("message", function(event) {
154
+ if (window.ReactNativeWebView && event.data) {
155
+ // ReactNativeWebView.postMessage only supports string data
156
+ window.ReactNativeWebView.postMessage(JSON.stringify(event.data));
157
+ }
158
+ }, false);
159
+ `}
160
+ onError={() => {
161
+ onDebugEvent('InlineAd: error loading iframe', { messageId })
162
+ }}
163
+ onLoad={() => {
164
+ onDebugEvent('InlineAd: iframe loaded', { messageId })
165
+ }}
166
+ />
167
+ )
168
+ }
@@ -0,0 +1,40 @@
1
+ import { type ConfigOptions, Configuration } from './Configuration'
2
+ import type { Message } from '@kontextso/sdk-common'
3
+ import { Session } from './Session'
4
+ import { Platform } from 'react-native'
5
+ import * as packageJson from '../package.json'
6
+
7
+ type GlobalConfig = Pick<ConfigOptions, 'publisherToken' | 'userId' | 'userEmail' | 'adServerUrl' | 'logLevel' | 'placementCode'>
8
+
9
+ type SessionConfig = Pick<ConfigOptions, 'conversationId' | 'character' | 'variantId' | 'regulatory'>
10
+
11
+ const KontextAds = (config: GlobalConfig) => {
12
+ return {
13
+ createSession: (sessionConfig: SessionConfig) => createSession({
14
+ ...config,
15
+ ...sessionConfig,
16
+ }),
17
+ }
18
+ }
19
+
20
+ const createSession = (options: ConfigOptions) => {
21
+ const sdk = {
22
+ name: 'sdk-react-native',
23
+ platform: Platform.OS === 'ios' ? 'ios' : 'android',
24
+ version: packageJson.version,
25
+ } as any
26
+ const instance = new Session(new Configuration(options), { sdk })
27
+ return {
28
+ addMessage: (message: Message) => {
29
+ instance.addMessage(message)
30
+ if (message.role === 'user') {
31
+ instance.preload().requestAd()
32
+ }
33
+ },
34
+ getInstance: () => instance,
35
+ }
36
+ }
37
+
38
+ export {
39
+ KontextAds
40
+ }
package/src/Logger.ts ADDED
@@ -0,0 +1,95 @@
1
+ export type LogLevel = 'debug' | 'info' | 'log' | 'warn' | 'error' | 'silent'
2
+
3
+ export class Logger {
4
+ private localLevel: LogLevel = 'log'
5
+ private remoteLevel: LogLevel = 'error'
6
+ private remoteConfig: { url: string; params: Record<string, any> } | null = null
7
+
8
+ private levels: Record<LogLevel, number> = {
9
+ debug: 0,
10
+ info: 1,
11
+ log: 2,
12
+ warn: 3,
13
+ error: 4,
14
+ silent: 5,
15
+ }
16
+
17
+ getLocalLevel() {
18
+ return this.localLevel
19
+ }
20
+
21
+ setLocalLevel(level: LogLevel) {
22
+ this.localLevel = level
23
+ }
24
+
25
+ getRemoteLevel() {
26
+ return this.remoteLevel
27
+ }
28
+
29
+ setRemoteLevel(level: LogLevel) {
30
+ this.remoteLevel = level
31
+ }
32
+
33
+ configureRemote(url: string, params: Record<string, any>) {
34
+ this.remoteConfig = { url, params }
35
+ }
36
+
37
+ private shouldLog(level: LogLevel, targetLevel: LogLevel): boolean {
38
+ if (targetLevel === 'silent') {
39
+ return false
40
+ }
41
+ return this.levels[level] >= this.levels[targetLevel]
42
+ }
43
+
44
+ private logToConsole(level: LogLevel, ...args: any[]) {
45
+ if (this.shouldLog(level, this.localLevel)) {
46
+ if (level === 'silent') {
47
+ return
48
+ }
49
+ console[level](...args)
50
+ }
51
+ }
52
+
53
+ private logToRemote(level: LogLevel, ...args: any[]) {
54
+ if (this.remoteConfig && this.shouldLog(level, this.remoteLevel)) {
55
+ // Simulate sending to remote server
56
+
57
+ fetch(`${this.remoteConfig.url}/log`, {
58
+ method: 'POST',
59
+ body: JSON.stringify({
60
+ ...this.remoteConfig.params,
61
+ level: level,
62
+ message: args,
63
+ timestamp: new Date().toISOString(),
64
+ }),
65
+ }).catch((e) => {
66
+ // ignore errors
67
+ })
68
+ }
69
+ }
70
+
71
+ debug(...args: any[]) {
72
+ this.logToConsole('debug', ...args)
73
+ this.logToRemote('debug', ...args)
74
+ }
75
+
76
+ info(...args: any[]) {
77
+ this.logToConsole('info', ...args)
78
+ this.logToRemote('info', ...args)
79
+ }
80
+
81
+ log(...args: any[]) {
82
+ this.logToConsole('log', ...args)
83
+ this.logToRemote('log', ...args)
84
+ }
85
+
86
+ warn(...args: any[]) {
87
+ this.logToConsole('warn', ...args)
88
+ this.logToRemote('warn', ...args)
89
+ }
90
+
91
+ error(...args: any[]) {
92
+ this.logToConsole('error', ...args)
93
+ this.logToRemote('error', ...args)
94
+ }
95
+ }
@@ -0,0 +1,20 @@
1
+ import { AbstractStream } from "./AbstractStream"
2
+
3
+ export class NativeStream extends AbstractStream {
4
+
5
+ render(messageId: string) {
6
+ const bid = this.session.getLastBid()
7
+ if (!bid) {
8
+ return null
9
+ }
10
+ if (bid.messageId !== messageId) {
11
+ return null
12
+ }
13
+ if (this.element) {
14
+ return this.element
15
+ }
16
+ this.setupMessageListener()
17
+ this.createIframe()
18
+ return this.element
19
+ }
20
+ }