@kontextso/sdk-react-native 2.1.1-rc.1 → 2.2.0-rc.2

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/dist/index.js CHANGED
@@ -36,9 +36,8 @@ __export(index_exports, {
36
36
  module.exports = __toCommonJS(index_exports);
37
37
 
38
38
  // src/formats/Format.tsx
39
- var import_react = require("react");
39
+ var import_react2 = require("react");
40
40
  var import_sdk_react = require("@kontextso/sdk-react");
41
- var import_react_native_webview = require("react-native-webview");
42
41
  var import_react_native = require("react-native");
43
42
 
44
43
  // ../sdk-common/dist/index.mjs
@@ -59,8 +58,44 @@ function handleIframeMessage(handler, opts) {
59
58
  };
60
59
  }
61
60
 
62
- // src/formats/Format.tsx
61
+ // src/frame-webview.tsx
62
+ var import_react = require("react");
63
+ var import_react_native_webview = require("react-native-webview");
63
64
  var import_jsx_runtime = require("react/jsx-runtime");
65
+ var FrameWebView = (0, import_react.forwardRef)(
66
+ ({ iframeUrl, onMessage, style, onError, onLoad }, forwardedRef) => {
67
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
68
+ import_react_native_webview.WebView,
69
+ {
70
+ ref: forwardedRef,
71
+ source: {
72
+ uri: iframeUrl
73
+ },
74
+ onMessage,
75
+ style,
76
+ allowsInlineMediaPlayback: true,
77
+ mediaPlaybackRequiresUserAction: false,
78
+ javaScriptEnabled: true,
79
+ domStorageEnabled: true,
80
+ allowsFullscreenVideo: false,
81
+ injectedJavaScript: `
82
+ window.addEventListener("message", function(event) {
83
+ if (window.ReactNativeWebView && event.data) {
84
+ // ReactNativeWebView.postMessage only supports string data
85
+ window.ReactNativeWebView.postMessage(JSON.stringify(event.data));
86
+ }
87
+ }, false);
88
+ `,
89
+ onError,
90
+ onLoad
91
+ }
92
+ );
93
+ }
94
+ );
95
+ var frame_webview_default = FrameWebView;
96
+
97
+ // src/formats/Format.tsx
98
+ var import_jsx_runtime2 = require("react/jsx-runtime");
64
99
  var sendMessage = (webViewRef, type, code, data) => {
65
100
  const message = makeIframeMessage(type, {
66
101
  data,
@@ -73,16 +108,22 @@ var sendMessage = (webViewRef, type, code, data) => {
73
108
  `);
74
109
  };
75
110
  var Format = ({ code, messageId, wrapper, ...otherParams }) => {
76
- const context = (0, import_react.useContext)(import_sdk_react.AdsContext);
111
+ const context = (0, import_react2.useContext)(import_sdk_react.AdsContext);
77
112
  const bid = (0, import_sdk_react.useBid)({ code, messageId });
78
- const [height, setHeight] = (0, import_react.useState)(0);
79
- const iframeUrl = (0, import_sdk_react.useIframeUrl)(context, bid, code, messageId);
80
- const [showIframe, setShowIframe] = (0, import_react.useState)(false);
81
- const [iframeLoaded, setIframeLoaded] = (0, import_react.useState)(false);
82
- const [containerStyles, setContainerStyles] = (0, import_react.useState)({});
83
- const [iframeStyles, setIframeStyles] = (0, import_react.useState)({});
84
- const containerRef = (0, import_react.useRef)(null);
85
- const webViewRef = (0, import_react.useRef)(null);
113
+ const [height, setHeight] = (0, import_react2.useState)(0);
114
+ const iframeUrl = (0, import_sdk_react.useIframeUrl)(context, bid, code, messageId, "sdk-react-native", otherParams.theme);
115
+ const modalUrl = iframeUrl.replace("/api/frame/", "/api/modal/");
116
+ const [showIframe, setShowIframe] = (0, import_react2.useState)(false);
117
+ const [iframeLoaded, setIframeLoaded] = (0, import_react2.useState)(false);
118
+ const [modalOpen, setModalOpen] = (0, import_react2.useState)(false);
119
+ const [modalShown, setModalShown] = (0, import_react2.useState)(false);
120
+ const [modalLoaded, setModalLoaded] = (0, import_react2.useState)(false);
121
+ const [containerStyles, setContainerStyles] = (0, import_react2.useState)({});
122
+ const [iframeStyles, setIframeStyles] = (0, import_react2.useState)({});
123
+ const containerRef = (0, import_react2.useRef)(null);
124
+ const webViewRef = (0, import_react2.useRef)(null);
125
+ const modalWebViewRef = (0, import_react2.useRef)(null);
126
+ const modalInitTimeoutRef = (0, import_react2.useRef)(null);
86
127
  const { height: windowHeight, width: windowWidth } = (0, import_react_native.useWindowDimensions)();
87
128
  const reset = () => {
88
129
  setHeight(0);
@@ -90,9 +131,19 @@ var Format = ({ code, messageId, wrapper, ...otherParams }) => {
90
131
  setContainerStyles({});
91
132
  setIframeStyles({});
92
133
  setIframeLoaded(false);
134
+ resetModal();
93
135
  context?.resetAll();
94
136
  context?.captureError(new Error("Processing iframe error"));
95
137
  };
138
+ const resetModal = () => {
139
+ if (modalInitTimeoutRef.current) {
140
+ clearTimeout(modalInitTimeoutRef.current);
141
+ modalInitTimeoutRef.current = null;
142
+ }
143
+ setModalOpen(false);
144
+ setModalLoaded(false);
145
+ setModalShown(false);
146
+ };
96
147
  const debug = (name, data = {}) => {
97
148
  context?.onDebugEventInternal?.(name, {
98
149
  code,
@@ -108,6 +159,19 @@ var Format = ({ code, messageId, wrapper, ...otherParams }) => {
108
159
  ...data
109
160
  });
110
161
  };
162
+ const debugModal = (name, data = {}) => {
163
+ context?.onDebugEventInternal?.(name, {
164
+ code,
165
+ messageId,
166
+ otherParams,
167
+ bid,
168
+ modalUrl,
169
+ modalOpen,
170
+ modalShown,
171
+ modalLoaded,
172
+ ...data
173
+ });
174
+ };
111
175
  debug("format-update-state");
112
176
  const onMessage = (event) => {
113
177
  try {
@@ -155,6 +219,10 @@ var Format = ({ code, messageId, wrapper, ...otherParams }) => {
155
219
  setContainerStyles(message.data.containerStyles);
156
220
  setIframeStyles(message.data.iframeStyles);
157
221
  break;
222
+ case "open-component-iframe":
223
+ setModalOpen(true);
224
+ modalInitTimeoutRef.current = setTimeout(resetModal, message.data.timeout ?? 5e3);
225
+ break;
158
226
  }
159
227
  },
160
228
  {
@@ -170,8 +238,55 @@ var Format = ({ code, messageId, wrapper, ...otherParams }) => {
170
238
  reset();
171
239
  }
172
240
  };
241
+ const onModalMessage = (event) => {
242
+ try {
243
+ const data = JSON.parse(event.nativeEvent.data);
244
+ debugModal("modal-iframe-message", {
245
+ message: data
246
+ });
247
+ const messageHandler = handleIframeMessage(
248
+ (message) => {
249
+ switch (message.type) {
250
+ case "close-component-iframe":
251
+ resetModal();
252
+ break;
253
+ case "init-component-iframe":
254
+ if (modalInitTimeoutRef.current) {
255
+ clearTimeout(modalInitTimeoutRef.current);
256
+ modalInitTimeoutRef.current = null;
257
+ }
258
+ setModalShown(true);
259
+ break;
260
+ case "error-component-iframe":
261
+ case "error-iframe":
262
+ resetModal();
263
+ break;
264
+ case "click-iframe":
265
+ if (message.data.url) {
266
+ import_react_native.Linking.openURL(`${context?.adServerUrl}${message.data.url}`).catch(
267
+ (err) => console.error("error opening url", err)
268
+ );
269
+ }
270
+ context?.onAdClickInternal(message.data);
271
+ break;
272
+ }
273
+ },
274
+ {
275
+ code,
276
+ component: "modal"
277
+ }
278
+ );
279
+ messageHandler({ data });
280
+ } catch (e) {
281
+ debugModal("modal-iframe-message-error", {
282
+ error: e
283
+ });
284
+ console.error("error parsing message from webview", e);
285
+ resetModal();
286
+ }
287
+ };
173
288
  const paramsString = (0, import_sdk_react.convertParamsToString)(otherParams);
174
- (0, import_react.useEffect)(() => {
289
+ (0, import_react2.useEffect)(() => {
175
290
  if (!iframeLoaded || !context?.adServerUrl || !bid || !webViewRef.current) {
176
291
  return;
177
292
  }
@@ -194,7 +309,7 @@ var Format = ({ code, messageId, wrapper, ...otherParams }) => {
194
309
  });
195
310
  });
196
311
  };
197
- (0, import_react.useEffect)(() => {
312
+ (0, import_react2.useEffect)(() => {
198
313
  const interval = setInterval(() => {
199
314
  checkIfInViewport();
200
315
  }, 250);
@@ -215,57 +330,71 @@ var Format = ({ code, messageId, wrapper, ...otherParams }) => {
215
330
  }
216
331
  return 0;
217
332
  };
218
- const content = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.View, { style: containerStyles, ref: containerRef, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
219
- import_react_native_webview.WebView,
220
- {
221
- ref: webViewRef,
222
- source: {
223
- uri: iframeUrl
224
- },
225
- onMessage,
226
- style: {
227
- height: getHeight(),
228
- width: getWidth(),
229
- ...iframeStyles
230
- },
231
- allowsInlineMediaPlayback: true,
232
- mediaPlaybackRequiresUserAction: false,
233
- javaScriptEnabled: true,
234
- domStorageEnabled: true,
235
- allowsFullscreenVideo: false,
236
- injectedJavaScript: `
237
- function sendToLog(data) {
238
- window.ReactNativeWebView.postMessage(JSON.stringify({
239
- type: 'log-iframe',
240
- data: data
241
- }));
242
- }
243
-
244
- window.addEventListener("message", function(event) {
245
- if (window.ReactNativeWebView && event.data) {
246
- // ReactNativeWebView.postMessage only supports string data
247
- window.ReactNativeWebView.postMessage(JSON.stringify(event.data));
333
+ const content = /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
334
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native.Modal, { visible: modalOpen, transparent: true, onRequestClose: resetModal, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
335
+ import_react_native.View,
336
+ {
337
+ style: {
338
+ flex: 1,
339
+ // Don't show the modal until the modal page is loaded and sends 'init-component-iframe' message back to SDK
340
+ ...modalShown ? { opacity: 1, pointerEvents: "auto" } : { opacity: 0, pointerEvents: "none" }
341
+ },
342
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
343
+ frame_webview_default,
344
+ {
345
+ ref: modalWebViewRef,
346
+ iframeUrl: modalUrl,
347
+ onMessage: onModalMessage,
348
+ style: {
349
+ backgroundColor: "transparent",
350
+ height: "100%",
351
+ width: "100%",
352
+ borderWidth: 0
353
+ },
354
+ onError: () => {
355
+ debug("modal-error");
356
+ resetModal();
357
+ },
358
+ onLoad: () => {
359
+ debug("modal-load");
360
+ setModalLoaded(true);
248
361
  }
249
- }, false);
250
- `,
251
- onError: () => {
252
- debug("iframe-error");
253
- reset();
254
- },
255
- onLoad: () => {
256
- debug("iframe-load");
362
+ }
363
+ )
257
364
  }
258
- }
259
- ) });
365
+ ) }),
366
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native.View, { style: containerStyles, ref: containerRef, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
367
+ frame_webview_default,
368
+ {
369
+ ref: webViewRef,
370
+ iframeUrl,
371
+ onMessage,
372
+ style: {
373
+ height: getHeight(),
374
+ width: getWidth(),
375
+ background: "transparent",
376
+ borderWidth: 0,
377
+ ...iframeStyles
378
+ },
379
+ onError: () => {
380
+ debug("iframe-error");
381
+ reset();
382
+ },
383
+ onLoad: () => {
384
+ debug("iframe-load");
385
+ }
386
+ }
387
+ ) })
388
+ ] });
260
389
  return wrapper ? wrapper(content) : content;
261
390
  };
262
- var FormatWithErrorBoundary = (props) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sdk_react.ErrorBoundary, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Format, { ...props }) });
391
+ var FormatWithErrorBoundary = (props) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sdk_react.ErrorBoundary, { children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Format, { ...props }) });
263
392
  var Format_default = FormatWithErrorBoundary;
264
393
 
265
394
  // src/formats/InlineAd.tsx
266
- var import_jsx_runtime2 = require("react/jsx-runtime");
395
+ var import_jsx_runtime3 = require("react/jsx-runtime");
267
396
  var InlineAd = ({ code, messageId, wrapper, ...props }) => {
268
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Format_default, { code, messageId, wrapper, ...props });
397
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Format_default, { code, messageId, wrapper, ...props });
269
398
  };
270
399
  var InlineAd_default = InlineAd;
271
400
 
@@ -279,7 +408,7 @@ var import_react_native2 = require("react-native");
279
408
  var NativeRNKontext_default = import_react_native2.TurboModuleRegistry.getEnforcing("RNKontext");
280
409
 
281
410
  // src/context/AdsProvider.tsx
282
- var import_jsx_runtime3 = require("react/jsx-runtime");
411
+ var import_jsx_runtime4 = require("react/jsx-runtime");
283
412
  ErrorUtils.setGlobalHandler((error, isFatal) => {
284
413
  if (!isFatal) {
285
414
  import_sdk_react2.log.warn(error);
@@ -323,7 +452,7 @@ var getDevice = async () => {
323
452
  return {};
324
453
  };
325
454
  var AdsProvider = (props) => {
326
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sdk_react2.AdsProviderInternal, { ...props, getDevice });
455
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_sdk_react2.AdsProviderInternal, { ...props, getDevice });
327
456
  };
328
457
  // Annotate the CommonJS export names for ESM import in node:
329
458
  0 && (module.exports = {
package/dist/index.mjs CHANGED
@@ -7,8 +7,7 @@ import {
7
7
  useBid,
8
8
  useIframeUrl
9
9
  } from "@kontextso/sdk-react";
10
- import { WebView } from "react-native-webview";
11
- import { Linking, View, useWindowDimensions } from "react-native";
10
+ import { Linking, Modal, View, useWindowDimensions } from "react-native";
12
11
 
13
12
  // ../sdk-common/dist/index.mjs
14
13
  function makeIframeMessage(type, opts) {
@@ -28,8 +27,44 @@ function handleIframeMessage(handler, opts) {
28
27
  };
29
28
  }
30
29
 
31
- // src/formats/Format.tsx
30
+ // src/frame-webview.tsx
31
+ import { forwardRef } from "react";
32
+ import { WebView } from "react-native-webview";
32
33
  import { jsx } from "react/jsx-runtime";
34
+ var FrameWebView = forwardRef(
35
+ ({ iframeUrl, onMessage, style, onError, onLoad }, forwardedRef) => {
36
+ return /* @__PURE__ */ jsx(
37
+ WebView,
38
+ {
39
+ ref: forwardedRef,
40
+ source: {
41
+ uri: iframeUrl
42
+ },
43
+ onMessage,
44
+ style,
45
+ allowsInlineMediaPlayback: true,
46
+ mediaPlaybackRequiresUserAction: false,
47
+ javaScriptEnabled: true,
48
+ domStorageEnabled: true,
49
+ allowsFullscreenVideo: false,
50
+ injectedJavaScript: `
51
+ window.addEventListener("message", function(event) {
52
+ if (window.ReactNativeWebView && event.data) {
53
+ // ReactNativeWebView.postMessage only supports string data
54
+ window.ReactNativeWebView.postMessage(JSON.stringify(event.data));
55
+ }
56
+ }, false);
57
+ `,
58
+ onError,
59
+ onLoad
60
+ }
61
+ );
62
+ }
63
+ );
64
+ var frame_webview_default = FrameWebView;
65
+
66
+ // src/formats/Format.tsx
67
+ import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
33
68
  var sendMessage = (webViewRef, type, code, data) => {
34
69
  const message = makeIframeMessage(type, {
35
70
  data,
@@ -45,13 +80,19 @@ var Format = ({ code, messageId, wrapper, ...otherParams }) => {
45
80
  const context = useContext(AdsContext);
46
81
  const bid = useBid({ code, messageId });
47
82
  const [height, setHeight] = useState(0);
48
- const iframeUrl = useIframeUrl(context, bid, code, messageId);
83
+ const iframeUrl = useIframeUrl(context, bid, code, messageId, "sdk-react-native", otherParams.theme);
84
+ const modalUrl = iframeUrl.replace("/api/frame/", "/api/modal/");
49
85
  const [showIframe, setShowIframe] = useState(false);
50
86
  const [iframeLoaded, setIframeLoaded] = useState(false);
87
+ const [modalOpen, setModalOpen] = useState(false);
88
+ const [modalShown, setModalShown] = useState(false);
89
+ const [modalLoaded, setModalLoaded] = useState(false);
51
90
  const [containerStyles, setContainerStyles] = useState({});
52
91
  const [iframeStyles, setIframeStyles] = useState({});
53
92
  const containerRef = useRef(null);
54
93
  const webViewRef = useRef(null);
94
+ const modalWebViewRef = useRef(null);
95
+ const modalInitTimeoutRef = useRef(null);
55
96
  const { height: windowHeight, width: windowWidth } = useWindowDimensions();
56
97
  const reset = () => {
57
98
  setHeight(0);
@@ -59,9 +100,19 @@ var Format = ({ code, messageId, wrapper, ...otherParams }) => {
59
100
  setContainerStyles({});
60
101
  setIframeStyles({});
61
102
  setIframeLoaded(false);
103
+ resetModal();
62
104
  context?.resetAll();
63
105
  context?.captureError(new Error("Processing iframe error"));
64
106
  };
107
+ const resetModal = () => {
108
+ if (modalInitTimeoutRef.current) {
109
+ clearTimeout(modalInitTimeoutRef.current);
110
+ modalInitTimeoutRef.current = null;
111
+ }
112
+ setModalOpen(false);
113
+ setModalLoaded(false);
114
+ setModalShown(false);
115
+ };
65
116
  const debug = (name, data = {}) => {
66
117
  context?.onDebugEventInternal?.(name, {
67
118
  code,
@@ -77,6 +128,19 @@ var Format = ({ code, messageId, wrapper, ...otherParams }) => {
77
128
  ...data
78
129
  });
79
130
  };
131
+ const debugModal = (name, data = {}) => {
132
+ context?.onDebugEventInternal?.(name, {
133
+ code,
134
+ messageId,
135
+ otherParams,
136
+ bid,
137
+ modalUrl,
138
+ modalOpen,
139
+ modalShown,
140
+ modalLoaded,
141
+ ...data
142
+ });
143
+ };
80
144
  debug("format-update-state");
81
145
  const onMessage = (event) => {
82
146
  try {
@@ -124,6 +188,10 @@ var Format = ({ code, messageId, wrapper, ...otherParams }) => {
124
188
  setContainerStyles(message.data.containerStyles);
125
189
  setIframeStyles(message.data.iframeStyles);
126
190
  break;
191
+ case "open-component-iframe":
192
+ setModalOpen(true);
193
+ modalInitTimeoutRef.current = setTimeout(resetModal, message.data.timeout ?? 5e3);
194
+ break;
127
195
  }
128
196
  },
129
197
  {
@@ -139,6 +207,53 @@ var Format = ({ code, messageId, wrapper, ...otherParams }) => {
139
207
  reset();
140
208
  }
141
209
  };
210
+ const onModalMessage = (event) => {
211
+ try {
212
+ const data = JSON.parse(event.nativeEvent.data);
213
+ debugModal("modal-iframe-message", {
214
+ message: data
215
+ });
216
+ const messageHandler = handleIframeMessage(
217
+ (message) => {
218
+ switch (message.type) {
219
+ case "close-component-iframe":
220
+ resetModal();
221
+ break;
222
+ case "init-component-iframe":
223
+ if (modalInitTimeoutRef.current) {
224
+ clearTimeout(modalInitTimeoutRef.current);
225
+ modalInitTimeoutRef.current = null;
226
+ }
227
+ setModalShown(true);
228
+ break;
229
+ case "error-component-iframe":
230
+ case "error-iframe":
231
+ resetModal();
232
+ break;
233
+ case "click-iframe":
234
+ if (message.data.url) {
235
+ Linking.openURL(`${context?.adServerUrl}${message.data.url}`).catch(
236
+ (err) => console.error("error opening url", err)
237
+ );
238
+ }
239
+ context?.onAdClickInternal(message.data);
240
+ break;
241
+ }
242
+ },
243
+ {
244
+ code,
245
+ component: "modal"
246
+ }
247
+ );
248
+ messageHandler({ data });
249
+ } catch (e) {
250
+ debugModal("modal-iframe-message-error", {
251
+ error: e
252
+ });
253
+ console.error("error parsing message from webview", e);
254
+ resetModal();
255
+ }
256
+ };
142
257
  const paramsString = convertParamsToString(otherParams);
143
258
  useEffect(() => {
144
259
  if (!iframeLoaded || !context?.adServerUrl || !bid || !webViewRef.current) {
@@ -184,57 +299,71 @@ var Format = ({ code, messageId, wrapper, ...otherParams }) => {
184
299
  }
185
300
  return 0;
186
301
  };
187
- const content = /* @__PURE__ */ jsx(View, { style: containerStyles, ref: containerRef, children: /* @__PURE__ */ jsx(
188
- WebView,
189
- {
190
- ref: webViewRef,
191
- source: {
192
- uri: iframeUrl
193
- },
194
- onMessage,
195
- style: {
196
- height: getHeight(),
197
- width: getWidth(),
198
- ...iframeStyles
199
- },
200
- allowsInlineMediaPlayback: true,
201
- mediaPlaybackRequiresUserAction: false,
202
- javaScriptEnabled: true,
203
- domStorageEnabled: true,
204
- allowsFullscreenVideo: false,
205
- injectedJavaScript: `
206
- function sendToLog(data) {
207
- window.ReactNativeWebView.postMessage(JSON.stringify({
208
- type: 'log-iframe',
209
- data: data
210
- }));
211
- }
212
-
213
- window.addEventListener("message", function(event) {
214
- if (window.ReactNativeWebView && event.data) {
215
- // ReactNativeWebView.postMessage only supports string data
216
- window.ReactNativeWebView.postMessage(JSON.stringify(event.data));
302
+ const content = /* @__PURE__ */ jsxs(Fragment, { children: [
303
+ /* @__PURE__ */ jsx2(Modal, { visible: modalOpen, transparent: true, onRequestClose: resetModal, children: /* @__PURE__ */ jsx2(
304
+ View,
305
+ {
306
+ style: {
307
+ flex: 1,
308
+ // Don't show the modal until the modal page is loaded and sends 'init-component-iframe' message back to SDK
309
+ ...modalShown ? { opacity: 1, pointerEvents: "auto" } : { opacity: 0, pointerEvents: "none" }
310
+ },
311
+ children: /* @__PURE__ */ jsx2(
312
+ frame_webview_default,
313
+ {
314
+ ref: modalWebViewRef,
315
+ iframeUrl: modalUrl,
316
+ onMessage: onModalMessage,
317
+ style: {
318
+ backgroundColor: "transparent",
319
+ height: "100%",
320
+ width: "100%",
321
+ borderWidth: 0
322
+ },
323
+ onError: () => {
324
+ debug("modal-error");
325
+ resetModal();
326
+ },
327
+ onLoad: () => {
328
+ debug("modal-load");
329
+ setModalLoaded(true);
217
330
  }
218
- }, false);
219
- `,
220
- onError: () => {
221
- debug("iframe-error");
222
- reset();
223
- },
224
- onLoad: () => {
225
- debug("iframe-load");
331
+ }
332
+ )
226
333
  }
227
- }
228
- ) });
334
+ ) }),
335
+ /* @__PURE__ */ jsx2(View, { style: containerStyles, ref: containerRef, children: /* @__PURE__ */ jsx2(
336
+ frame_webview_default,
337
+ {
338
+ ref: webViewRef,
339
+ iframeUrl,
340
+ onMessage,
341
+ style: {
342
+ height: getHeight(),
343
+ width: getWidth(),
344
+ background: "transparent",
345
+ borderWidth: 0,
346
+ ...iframeStyles
347
+ },
348
+ onError: () => {
349
+ debug("iframe-error");
350
+ reset();
351
+ },
352
+ onLoad: () => {
353
+ debug("iframe-load");
354
+ }
355
+ }
356
+ ) })
357
+ ] });
229
358
  return wrapper ? wrapper(content) : content;
230
359
  };
231
- var FormatWithErrorBoundary = (props) => /* @__PURE__ */ jsx(ErrorBoundary, { children: /* @__PURE__ */ jsx(Format, { ...props }) });
360
+ var FormatWithErrorBoundary = (props) => /* @__PURE__ */ jsx2(ErrorBoundary, { children: /* @__PURE__ */ jsx2(Format, { ...props }) });
232
361
  var Format_default = FormatWithErrorBoundary;
233
362
 
234
363
  // src/formats/InlineAd.tsx
235
- import { jsx as jsx2 } from "react/jsx-runtime";
364
+ import { jsx as jsx3 } from "react/jsx-runtime";
236
365
  var InlineAd = ({ code, messageId, wrapper, ...props }) => {
237
- return /* @__PURE__ */ jsx2(Format_default, { code, messageId, wrapper, ...props });
366
+ return /* @__PURE__ */ jsx3(Format_default, { code, messageId, wrapper, ...props });
238
367
  };
239
368
  var InlineAd_default = InlineAd;
240
369
 
@@ -248,7 +377,7 @@ import { TurboModuleRegistry } from "react-native";
248
377
  var NativeRNKontext_default = TurboModuleRegistry.getEnforcing("RNKontext");
249
378
 
250
379
  // src/context/AdsProvider.tsx
251
- import { jsx as jsx3 } from "react/jsx-runtime";
380
+ import { jsx as jsx4 } from "react/jsx-runtime";
252
381
  ErrorUtils.setGlobalHandler((error, isFatal) => {
253
382
  if (!isFatal) {
254
383
  log.warn(error);
@@ -292,7 +421,7 @@ var getDevice = async () => {
292
421
  return {};
293
422
  };
294
423
  var AdsProvider = (props) => {
295
- return /* @__PURE__ */ jsx3(AdsProviderInternal, { ...props, getDevice });
424
+ return /* @__PURE__ */ jsx4(AdsProviderInternal, { ...props, getDevice });
296
425
  };
297
426
  export {
298
427
  AdsProvider,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kontextso/sdk-react-native",
3
- "version": "2.1.1-rc.1",
3
+ "version": "2.2.0-rc.2",
4
4
  "description": "Kontext SDK for React Native",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -55,7 +55,7 @@
55
55
  "react-native-webview": "^13.15.0"
56
56
  },
57
57
  "dependencies": {
58
- "@kontextso/sdk-react": "^1.2.3"
58
+ "@kontextso/sdk-react": "^1.2.4"
59
59
  },
60
60
  "files": [
61
61
  "dist/*",
@@ -32,8 +32,6 @@ const getDevice = async (): Promise<Device> => {
32
32
  log.warn('Failed to read output volume', error)
33
33
  }
34
34
 
35
- console.log('soundOn', soundOn)
36
-
37
35
  const rnv = Platform.constants.reactNativeVersion
38
36
  const reactNativeVersion = `${rnv.major}.${rnv.minor}.${rnv.patch}`
39
37
 
@@ -7,15 +7,16 @@ import {
7
7
  useIframeUrl,
8
8
  type FormatProps,
9
9
  } from '@kontextso/sdk-react'
10
- import { WebView, type WebViewMessageEvent } from 'react-native-webview'
11
- import { Linking, View, useWindowDimensions } from 'react-native'
10
+ import type { WebView, WebViewMessageEvent } from 'react-native-webview'
11
+ import { Linking, Modal, View, useWindowDimensions } from 'react-native'
12
12
  import {
13
13
  handleIframeMessage,
14
- IframeMessageEvent,
15
- IframeMessageType,
14
+ type IframeMessageEvent,
15
+ type IframeMessageType,
16
16
  makeIframeMessage,
17
17
  type IframeMessage,
18
18
  } from '@kontextso/sdk-common'
19
+ import FrameWebView from '../frame-webview'
19
20
 
20
21
  const sendMessage = (
21
22
  webViewRef: React.RefObject<WebView>,
@@ -40,16 +41,25 @@ const Format = ({ code, messageId, wrapper, ...otherParams }: FormatProps) => {
40
41
 
41
42
  const bid = useBid({ code, messageId })
42
43
  const [height, setHeight] = useState<number>(0)
43
- const iframeUrl = useIframeUrl(context, bid, code, messageId)
44
+
45
+ const iframeUrl = useIframeUrl(context, bid, code, messageId, 'sdk-react-native', otherParams.theme)
46
+ const modalUrl = iframeUrl.replace('/api/frame/', '/api/modal/')
44
47
 
45
48
  const [showIframe, setShowIframe] = useState<boolean>(false)
46
49
  const [iframeLoaded, setIframeLoaded] = useState<boolean>(false)
47
50
 
51
+ const [modalOpen, setModalOpen] = useState<boolean>(false)
52
+ const [modalShown, setModalShown] = useState<boolean>(false)
53
+ const [modalLoaded, setModalLoaded] = useState<boolean>(false)
54
+
48
55
  const [containerStyles, setContainerStyles] = useState<any>({})
49
56
  const [iframeStyles, setIframeStyles] = useState<any>({})
50
57
 
51
58
  const containerRef = useRef<View>(null)
52
59
  const webViewRef = useRef<WebView>(null)
60
+ const modalWebViewRef = useRef<WebView>(null)
61
+ const modalInitTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
62
+ const isModalInitRef = useRef<boolean>(false)
53
63
 
54
64
  const { height: windowHeight, width: windowWidth } = useWindowDimensions()
55
65
 
@@ -59,10 +69,23 @@ const Format = ({ code, messageId, wrapper, ...otherParams }: FormatProps) => {
59
69
  setContainerStyles({})
60
70
  setIframeStyles({})
61
71
  setIframeLoaded(false)
72
+ resetModal()
62
73
  context?.resetAll()
63
74
  context?.captureError(new Error('Processing iframe error'))
64
75
  }
65
76
 
77
+ const resetModal = () => {
78
+ if (modalInitTimeoutRef.current) {
79
+ clearTimeout(modalInitTimeoutRef.current)
80
+ modalInitTimeoutRef.current = null
81
+ }
82
+
83
+ isModalInitRef.current = false
84
+ setModalOpen(false)
85
+ setModalLoaded(false)
86
+ setModalShown(false)
87
+ }
88
+
66
89
  const debug = (name: string, data: any = {}) => {
67
90
  context?.onDebugEventInternal?.(name, {
68
91
  code,
@@ -79,6 +102,20 @@ const Format = ({ code, messageId, wrapper, ...otherParams }: FormatProps) => {
79
102
  })
80
103
  }
81
104
 
105
+ const debugModal = (name: string, data: any = {}) => {
106
+ context?.onDebugEventInternal?.(name, {
107
+ code,
108
+ messageId,
109
+ otherParams,
110
+ bid,
111
+ modalUrl,
112
+ modalOpen,
113
+ modalShown,
114
+ modalLoaded,
115
+ ...data,
116
+ })
117
+ }
118
+
82
119
  debug('format-update-state')
83
120
 
84
121
  const onMessage = (event: WebViewMessageEvent) => {
@@ -136,6 +173,16 @@ const Format = ({ code, messageId, wrapper, ...otherParams }: FormatProps) => {
136
173
  setContainerStyles(message.data.containerStyles)
137
174
  setIframeStyles(message.data.iframeStyles)
138
175
  break
176
+
177
+ case 'open-component-iframe':
178
+ setModalOpen(true)
179
+
180
+ modalInitTimeoutRef.current = setTimeout(() => {
181
+ if (!isModalInitRef.current) {
182
+ resetModal()
183
+ }
184
+ }, message.data.timeout ?? 5000)
185
+ break
139
186
  }
140
187
  },
141
188
  {
@@ -152,6 +199,64 @@ const Format = ({ code, messageId, wrapper, ...otherParams }: FormatProps) => {
152
199
  }
153
200
  }
154
201
 
202
+ const onModalMessage = (event: WebViewMessageEvent) => {
203
+ try {
204
+ const data = JSON.parse(event.nativeEvent.data) as IframeMessage
205
+
206
+ debugModal('modal-iframe-message', {
207
+ message: data,
208
+ })
209
+
210
+ const messageHandler = handleIframeMessage(
211
+ (message) => {
212
+ switch (message.type) {
213
+ case 'close-component-iframe':
214
+ resetModal()
215
+ break
216
+
217
+ case 'init-component-iframe':
218
+ // Just clearing the timeoutRef didn't work in Android, so we need to set a flag and check it in the timeout callback
219
+ isModalInitRef.current = true
220
+
221
+ if (modalInitTimeoutRef.current) {
222
+ clearTimeout(modalInitTimeoutRef.current)
223
+ modalInitTimeoutRef.current = null
224
+ }
225
+
226
+ setModalShown(true)
227
+ break
228
+
229
+ case 'error-component-iframe':
230
+ case 'error-iframe':
231
+ resetModal()
232
+ context?.captureError(new Error('Processing modal iframe error'))
233
+ break
234
+
235
+ case 'click-iframe':
236
+ if (message.data.url) {
237
+ Linking.openURL(`${context?.adServerUrl}${message.data.url}`).catch((err) =>
238
+ console.error('error opening url', err)
239
+ )
240
+ }
241
+ context?.onAdClickInternal(message.data)
242
+ break
243
+ }
244
+ },
245
+ {
246
+ code,
247
+ component: 'modal',
248
+ }
249
+ )
250
+ messageHandler({ data } as IframeMessageEvent)
251
+ } catch (e) {
252
+ debugModal('modal-iframe-message-error', {
253
+ error: e,
254
+ })
255
+ console.error('error parsing message from webview', e)
256
+ resetModal()
257
+ }
258
+ }
259
+
155
260
  const paramsString = convertParamsToString(otherParams)
156
261
 
157
262
  useEffect(() => {
@@ -208,47 +313,65 @@ const Format = ({ code, messageId, wrapper, ...otherParams }: FormatProps) => {
208
313
  }
209
314
 
210
315
  const content = (
211
- <View style={containerStyles} ref={containerRef}>
212
- <WebView
213
- ref={webViewRef}
214
- source={{
215
- uri: iframeUrl,
216
- }}
217
- onMessage={onMessage}
218
- style={{
219
- height: getHeight(),
220
- width: getWidth(),
221
- ...iframeStyles,
222
- }}
223
- allowsInlineMediaPlayback={true}
224
- mediaPlaybackRequiresUserAction={false}
225
- javaScriptEnabled={true}
226
- domStorageEnabled={true}
227
- allowsFullscreenVideo={false}
228
- injectedJavaScript={`
229
- function sendToLog(data) {
230
- window.ReactNativeWebView.postMessage(JSON.stringify({
231
- type: 'log-iframe',
232
- data: data
233
- }));
234
- }
235
-
236
- window.addEventListener("message", function(event) {
237
- if (window.ReactNativeWebView && event.data) {
238
- // ReactNativeWebView.postMessage only supports string data
239
- window.ReactNativeWebView.postMessage(JSON.stringify(event.data));
240
- }
241
- }, false);
242
- `}
243
- onError={() => {
244
- debug('iframe-error')
245
- reset()
246
- }}
247
- onLoad={() => {
248
- debug('iframe-load')
249
- }}
250
- />
251
- </View>
316
+ <>
317
+ <Modal
318
+ visible={modalOpen}
319
+ transparent={true}
320
+ onRequestClose={resetModal}
321
+ animationType="slide"
322
+ statusBarTranslucent={true}
323
+ >
324
+ <View
325
+ style={{
326
+ flex: 1,
327
+ // Don't show the modal until the modal page is loaded and sends 'init-component-iframe' message back to SDK
328
+ ...(modalShown ? { opacity: 1, pointerEvents: 'auto' } : { opacity: 0, pointerEvents: 'none' }),
329
+ }}
330
+ >
331
+ <FrameWebView
332
+ ref={modalWebViewRef}
333
+ iframeUrl={modalUrl}
334
+ onMessage={onModalMessage}
335
+ style={{
336
+ backgroundColor: 'transparent',
337
+ height: '100%',
338
+ width: '100%',
339
+ borderWidth: 0,
340
+ }}
341
+ onError={() => {
342
+ debug('modal-error')
343
+ resetModal()
344
+ }}
345
+ onLoad={() => {
346
+ debug('modal-load')
347
+ setModalLoaded(true)
348
+ }}
349
+ />
350
+ </View>
351
+ </Modal>
352
+
353
+ <View style={containerStyles} ref={containerRef}>
354
+ <FrameWebView
355
+ ref={webViewRef}
356
+ iframeUrl={iframeUrl}
357
+ onMessage={onMessage}
358
+ style={{
359
+ height: getHeight(),
360
+ width: getWidth(),
361
+ backgroundColor: 'transparent',
362
+ borderWidth: 0,
363
+ ...iframeStyles,
364
+ }}
365
+ onError={() => {
366
+ debug('iframe-error')
367
+ reset()
368
+ }}
369
+ onLoad={() => {
370
+ debug('iframe-load')
371
+ }}
372
+ />
373
+ </View>
374
+ </>
252
375
  )
253
376
 
254
377
  return wrapper ? wrapper(content) : content
@@ -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