@kontextso/sdk-react-native 3.0.7-rc.3 → 3.0.7

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.
@@ -7,12 +7,17 @@ import {
7
7
  } from '@kontextso/sdk-common'
8
8
  import {
9
9
  AdsContext,
10
+ type ContextType,
11
+ convertParamsToString,
12
+ ErrorBoundary,
10
13
  type FormatProps,
11
14
  useBid,
15
+ useIframeUrl,
12
16
  } from '@kontextso/sdk-react'
13
- import { useContext, useRef } from 'react'
14
- import { View } from 'react-native'
15
- import { WebView, type WebViewMessageEvent } from 'react-native-webview'
17
+ import { useContext, useEffect, useRef, useState } from 'react'
18
+ import { Keyboard, Linking, Modal, useWindowDimensions, View } from 'react-native'
19
+ import type { WebView, WebViewMessageEvent } from 'react-native-webview'
20
+ import FrameWebView from '../frame-webview'
16
21
 
17
22
  const sendMessage = (
18
23
  webViewRef: React.RefObject<WebView>,
@@ -32,30 +37,68 @@ const sendMessage = (
32
37
  `)
33
38
  }
34
39
 
35
- const getUrl = (code: string, messageId: string, bidId?: string) => {
36
- const context = useContext(AdsContext)
37
- if (!context || !bidId) {
40
+ const getCachedContent = (context: ContextType, bidId?: string) => {
41
+ if (!bidId) {
38
42
  return null
39
43
  }
40
-
41
- const adServerUrl = context?.adServerUrl
42
-
43
- const params = new URLSearchParams({
44
- code,
45
- messageId,
46
- sdk: 'sdk-react-native',
47
- })
48
-
49
- return `${adServerUrl}/api/frame/${bidId}?${params}`
44
+ return context?.cachedContentRef?.current?.get(bidId) ?? null
50
45
  }
51
46
 
52
47
  const Format = ({ code, messageId, wrapper, onEvent, ...otherParams }: FormatProps) => {
53
48
  const context = useContext(AdsContext)
54
49
 
55
50
  const bid = useBid({ code, messageId })
56
- const iframeUrl = getUrl(code, messageId, bid?.bidId)
51
+ const [height, setHeight] = useState<number>(0)
52
+
53
+ const cachedContent = getCachedContent(context, bid?.bidId)
54
+
55
+ const iframeUrl = useIframeUrl(bid, code, messageId, 'sdk-react-native', otherParams.theme, cachedContent)
56
+ const modalUrl = iframeUrl.replace('/api/frame/', '/api/modal/')
57
+
58
+ const [showIframe, setShowIframe] = useState<boolean>(false)
59
+ const [iframeLoaded, setIframeLoaded] = useState<boolean>(false)
57
60
 
61
+ const [modalOpen, setModalOpen] = useState<boolean>(false)
62
+ const [modalShown, setModalShown] = useState<boolean>(false)
63
+ const [modalLoaded, setModalLoaded] = useState<boolean>(false)
64
+
65
+ const [containerStyles, setContainerStyles] = useState<any>({})
66
+ const [iframeStyles, setIframeStyles] = useState<any>({})
67
+
68
+ const containerRef = useRef<View>(null)
58
69
  const webViewRef = useRef<WebView>(null)
70
+ const modalWebViewRef = useRef<WebView>(null)
71
+ const modalInitTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
72
+ const isModalInitRef = useRef<boolean>(false)
73
+
74
+ const { height: windowHeight, width: windowWidth } = useWindowDimensions()
75
+
76
+ const keyboardHeightRef = useRef(0)
77
+
78
+ const isAdViewVisible = showIframe && iframeLoaded
79
+
80
+ const reset = () => {
81
+ setHeight(0)
82
+ setShowIframe(false)
83
+ setContainerStyles({})
84
+ setIframeStyles({})
85
+ setIframeLoaded(false)
86
+ resetModal()
87
+ context?.resetAll()
88
+ context?.captureError(new Error('Processing iframe error'))
89
+ }
90
+
91
+ const resetModal = () => {
92
+ if (modalInitTimeoutRef.current) {
93
+ clearTimeout(modalInitTimeoutRef.current)
94
+ modalInitTimeoutRef.current = null
95
+ }
96
+
97
+ isModalInitRef.current = false
98
+ setModalOpen(false)
99
+ setModalLoaded(false)
100
+ setModalShown(false)
101
+ }
59
102
 
60
103
  const debug = (name: string, data: any = {}) => {
61
104
  context?.onDebugEventInternal?.(name, {
@@ -64,40 +107,45 @@ const Format = ({ code, messageId, wrapper, onEvent, ...otherParams }: FormatPro
64
107
  otherParams,
65
108
  bid,
66
109
  iframeUrl,
110
+ iframeLoaded,
111
+ showIframe,
112
+ height,
113
+ containerStyles,
114
+ iframeStyles,
67
115
  ...data,
68
116
  })
69
117
  }
70
118
 
71
- debug('Format:updateState', {
72
- params: {
73
- messageId,
119
+ const debugModal = (name: string, data: any = {}) => {
120
+ context?.onDebugEventInternal?.(name, {
74
121
  code,
75
- otherParams
76
- }
77
- })
122
+ messageId,
123
+ otherParams,
124
+ bid,
125
+ modalUrl,
126
+ modalOpen,
127
+ modalShown,
128
+ modalLoaded,
129
+ ...data,
130
+ })
131
+ }
132
+
133
+ debug('format-update-state')
78
134
 
79
135
  const onMessage = (event: WebViewMessageEvent) => {
80
136
  try {
81
137
  const data = JSON.parse(event.nativeEvent.data) as IframeMessage
82
138
 
83
- debug('Format:iframeMessage', {
139
+ debug('iframe-message', {
84
140
  message: data,
85
- params: { data, messageId, code, otherParams }
86
141
  })
87
142
 
88
143
  const messageHandler = handleIframeMessage(
89
144
  (message) => {
90
145
  switch (message.type) {
91
146
  case 'init-iframe':
92
- debug('Format:iframePostMessage', {
93
- params: {
94
- code,
95
- messages: context?.messages,
96
- sdk: 'sdk-react-native',
97
- otherParams,
98
- messageId,
99
- }
100
- })
147
+ setIframeLoaded(true)
148
+ debug('iframe-post-message')
101
149
  sendMessage(webViewRef, 'update-iframe', code, {
102
150
  messages: context?.messages,
103
151
  sdk: 'sdk-react-native',
@@ -106,6 +154,56 @@ const Format = ({ code, messageId, wrapper, onEvent, ...otherParams }: FormatPro
106
154
  })
107
155
  break
108
156
 
157
+ case 'error-iframe':
158
+ reset()
159
+ break
160
+
161
+ case 'resize-iframe':
162
+ setHeight(message.data.height)
163
+ break
164
+
165
+ case 'click-iframe':
166
+ if (message.data.url) {
167
+ Linking.openURL(`${context?.adServerUrl}${message.data.url}`).catch((err) =>
168
+ console.error('error opening url', err)
169
+ )
170
+ }
171
+ context?.onAdClickInternal(message.data)
172
+ break
173
+
174
+ case 'view-iframe':
175
+ context?.onAdViewInternal(message.data)
176
+ break
177
+
178
+ case 'ad-done-iframe':
179
+ if (bid?.bidId && message.data.cachedContent) {
180
+ context?.cachedContentRef?.current?.set(bid.bidId, message.data.cachedContent)
181
+ }
182
+ break
183
+
184
+ case 'show-iframe':
185
+ setShowIframe(true)
186
+ break
187
+
188
+ case 'hide-iframe':
189
+ setShowIframe(false)
190
+ break
191
+
192
+ case 'set-styles-iframe':
193
+ setContainerStyles(message.data.containerStyles)
194
+ setIframeStyles(message.data.iframeStyles)
195
+ break
196
+
197
+ case 'open-component-iframe':
198
+ setModalOpen(true)
199
+
200
+ modalInitTimeoutRef.current = setTimeout(() => {
201
+ if (!isModalInitRef.current) {
202
+ resetModal()
203
+ }
204
+ }, message.data.timeout ?? 5000)
205
+ break
206
+
109
207
  case 'event-iframe':
110
208
  onEvent?.(message.data)
111
209
  context?.onAdEventInternal(message.data)
@@ -118,139 +216,221 @@ const Format = ({ code, messageId, wrapper, onEvent, ...otherParams }: FormatPro
118
216
  )
119
217
  messageHandler({ data } as IframeMessageEvent)
120
218
  } catch (e) {
121
- debug('Format:iframeMessageError', {
122
- params: { error: e, messageId, code, otherParams },
219
+ debug('iframe-message-error', {
123
220
  error: e,
124
221
  })
125
222
  console.error('error parsing message from webview', e)
223
+ reset()
126
224
  }
127
225
  }
128
226
 
129
- if (!context || !bid || !iframeUrl) {
130
- debug('Format:noContextOrBidOrIframeUrl', {
131
- params: {
132
- context,
133
- bid,
134
- iframeUrl,
135
- messageId,
136
- code,
137
- otherParams,
138
- }
227
+ const onModalMessage = (event: WebViewMessageEvent) => {
228
+ try {
229
+ const data = JSON.parse(event.nativeEvent.data) as IframeMessage
230
+
231
+ debugModal('modal-iframe-message', {
232
+ message: data,
233
+ })
234
+
235
+ const messageHandler = handleIframeMessage(
236
+ (message) => {
237
+ switch (message.type) {
238
+ case 'close-component-iframe':
239
+ resetModal()
240
+ break
241
+
242
+ case 'init-component-iframe':
243
+ // Just clearing the timeoutRef didn't work in Android, so we need to set a flag and check it in the timeout callback
244
+ isModalInitRef.current = true
245
+
246
+ if (modalInitTimeoutRef.current) {
247
+ clearTimeout(modalInitTimeoutRef.current)
248
+ modalInitTimeoutRef.current = null
249
+ }
250
+
251
+ setModalShown(true)
252
+ break
253
+
254
+ case 'error-component-iframe':
255
+ case 'error-iframe':
256
+ resetModal()
257
+ context?.captureError(new Error('Processing modal iframe error'))
258
+ break
259
+
260
+ case 'click-iframe':
261
+ if (message.data.url) {
262
+ Linking.openURL(`${context?.adServerUrl}${message.data.url}`).catch((err) =>
263
+ console.error('error opening url', err)
264
+ )
265
+ }
266
+ context?.onAdClickInternal(message.data)
267
+ break
268
+
269
+ case 'event-iframe':
270
+ onEvent?.(message.data)
271
+ context?.onAdEventInternal(message.data)
272
+ break
273
+ }
274
+ },
275
+ {
276
+ code,
277
+ component: 'modal',
278
+ }
279
+ )
280
+ messageHandler({ data } as IframeMessageEvent)
281
+ } catch (e) {
282
+ debugModal('modal-iframe-message-error', {
283
+ error: e,
284
+ })
285
+ console.error('error parsing message from webview', e)
286
+ resetModal()
287
+ }
288
+ }
289
+
290
+ const paramsString = convertParamsToString(otherParams)
291
+
292
+ useEffect(() => {
293
+ if (!iframeLoaded || !context?.adServerUrl || !bid || !webViewRef.current) {
294
+ return
295
+ }
296
+ debug('iframe-post-message')
297
+ sendMessage(webViewRef, 'update-iframe', code, {
298
+ data: { otherParams },
299
+ code,
300
+ })
301
+ // because we use the rest params, the object is alaways new and useEffect would be called on every render
302
+ }, [paramsString, iframeLoaded, context?.adServerUrl, bid, code])
303
+
304
+ const checkIfInViewport = () => {
305
+ if (!containerRef.current) return
306
+
307
+ containerRef.current.measureInWindow((containerX, containerY, containerWidth, containerHeight) => {
308
+ sendMessage(webViewRef, 'update-dimensions-iframe', code, {
309
+ windowWidth,
310
+ windowHeight,
311
+ containerWidth,
312
+ containerHeight,
313
+ containerX,
314
+ containerY,
315
+ keyboardHeight: keyboardHeightRef.current,
316
+ })
139
317
  })
318
+ }
319
+
320
+ useEffect(() => {
321
+ if (!isAdViewVisible) return
322
+
323
+ const interval = setInterval(() => {
324
+ checkIfInViewport()
325
+ }, 250)
326
+
327
+ return () => clearInterval(interval)
328
+ }, [isAdViewVisible])
329
+
330
+ useEffect(() => {
331
+ const showSubscription = Keyboard.addListener('keyboardDidShow', (e) => {
332
+ keyboardHeightRef.current = e?.endCoordinates?.height ?? 0
333
+ })
334
+ const hideSubscription = Keyboard.addListener('keyboardDidHide', () => {
335
+ keyboardHeightRef.current = 0
336
+ })
337
+
338
+ return () => {
339
+ showSubscription.remove()
340
+ hideSubscription.remove()
341
+ keyboardHeightRef.current = 0
342
+ }
343
+ }, [])
344
+
345
+ if (!context || !bid || !iframeUrl) {
140
346
  return null
141
347
  }
142
348
 
143
- return (
144
- <View
349
+ const inlineContent = (
350
+ <FrameWebView
351
+ ref={webViewRef}
352
+ iframeUrl={iframeUrl}
353
+ onMessage={onMessage}
145
354
  style={{
146
- height: 300,
355
+ height,
147
356
  width: '100%',
148
357
  backgroundColor: 'transparent',
149
358
  borderWidth: 0,
359
+ ...iframeStyles,
150
360
  }}
151
- >
361
+ onError={() => {
362
+ debug('iframe-error')
363
+ reset()
364
+ }}
365
+ onLoad={() => {
366
+ debug('iframe-load')
367
+ }}
368
+ />
369
+ )
152
370
 
153
- <WebView
154
- ref={webViewRef}
155
- source={{
156
- uri: iframeUrl,
157
- }}
158
- onMessage={onMessage}
371
+ const interstitialContent = (
372
+ <Modal
373
+ visible={modalOpen}
374
+ transparent={true}
375
+ onRequestClose={resetModal}
376
+ animationType="slide"
377
+ statusBarTranslucent={true}
378
+ >
379
+ <View
159
380
  style={{
160
- height: 300,
161
- width: '100%',
162
- backgroundColor: 'transparent',
163
- borderWidth: 0,
164
- }}
165
- allowsInlineMediaPlayback={true}
166
- mediaPlaybackRequiresUserAction={false}
167
- javaScriptEnabled={true}
168
- domStorageEnabled={true}
169
- allowsFullscreenVideo={false}
170
- injectedJavaScript={`
171
- window.addEventListener("message", function(event) {
172
- if (window.ReactNativeWebView && event.data) {
173
- // ReactNativeWebView.postMessage only supports string data
174
- window.ReactNativeWebView.postMessage(JSON.stringify(event.data));
175
- }
176
- }, false);
177
- `}
178
- onLoadStart={() => {
179
- debug('Format:iframeLoadStart', {
180
- params: {
181
- messageId,
182
- code,
183
- otherParams,
184
- }
185
- })
186
- }}
187
- onError={() => {
188
- debug('Format:iframeError', {
189
- params: {
190
- messageId,
191
- code,
192
- otherParams,
193
- }
194
- })
195
- }}
196
- onLoad={() => {
197
- debug('Format:iframeLoad', {
198
- params: {
199
- messageId,
200
- code,
201
- otherParams,
202
- }
203
- })
204
- }}
205
- onLoadProgress={() => {
206
- debug('Format:iframeLoadProgress', {
207
- params: {
208
- messageId,
209
- code,
210
- otherParams,
211
- }
212
- })
213
- }}
214
- onHttpError={() => {
215
- debug('Format:iframeHttpError', {
216
- params: {
217
- messageId,
218
- code,
219
- otherParams,
220
- }
221
- })
222
- }}
223
- onRenderProcessGone={() => {
224
- debug('Format:iframeRenderProcessGone', {
225
- params: {
226
- messageId,
227
- code,
228
- otherParams,
229
- }
230
- })
381
+ flex: 1,
382
+ // Don't show the modal until the modal page is loaded and sends 'init-component-iframe' message back to SDK
383
+ ...(modalShown ? { opacity: 1, pointerEvents: 'auto' } : { opacity: 0, pointerEvents: 'none' }),
231
384
  }}
232
- onNavigationStateChange={() => {
233
- debug('Format:iframeNavigationStateChange', {
234
- params: {
235
- messageId,
236
- code,
237
- otherParams,
238
- }
239
- })
240
- }}
241
- onContentProcessDidTerminate={() => {
242
- debug('Format:iframeContentProcessDidTerminate', {
243
- params: {
244
- messageId,
245
- code,
246
- otherParams,
247
- }
248
- })
249
- }}
250
-
251
- />
252
- </View>
385
+ >
386
+ <FrameWebView
387
+ ref={modalWebViewRef}
388
+ iframeUrl={modalUrl}
389
+ onMessage={onModalMessage}
390
+ style={{
391
+ backgroundColor: 'transparent',
392
+ height: '100%',
393
+ width: '100%',
394
+ borderWidth: 0,
395
+ }}
396
+ onError={() => {
397
+ debug('modal-error')
398
+ resetModal()
399
+ }}
400
+ onLoad={() => {
401
+ debug('modal-load')
402
+ setModalLoaded(true)
403
+ }}
404
+ />
405
+ </View>
406
+ </Modal>
407
+ )
408
+
409
+ return (
410
+ <>
411
+ <View
412
+ style={
413
+ isAdViewVisible
414
+ ? containerStyles
415
+ : {
416
+ height: 0,
417
+ overflow: 'hidden',
418
+ }
419
+ }
420
+ ref={containerRef}
421
+ >
422
+ {wrapper ? wrapper(inlineContent) : inlineContent}
423
+ </View>
424
+
425
+ {interstitialContent}
426
+ </>
253
427
  )
254
428
  }
255
429
 
256
- export default Format
430
+ const FormatWithErrorBoundary = (props: FormatProps) => (
431
+ <ErrorBoundary>
432
+ <Format {...props} />
433
+ </ErrorBoundary>
434
+ )
435
+
436
+ export default FormatWithErrorBoundary
@@ -25,6 +25,9 @@ const FrameWebView = forwardRef<WebView, FrameWebViewProps>(
25
25
  javaScriptEnabled={true}
26
26
  domStorageEnabled={true}
27
27
  allowsFullscreenVideo={false}
28
+ originWhitelist={['*']}
29
+ sharedCookiesEnabled={true}
30
+ thirdPartyCookiesEnabled={true}
28
31
  injectedJavaScript={`
29
32
  window.addEventListener("message", function(event) {
30
33
  if (window.ReactNativeWebView && event.data) {