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