@adadapted/react-native-sdk 3.1.12 → 3.2.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.
@@ -3,7 +3,13 @@
3
3
  * @module
4
4
  */
5
5
  import * as React from "react";
6
- import { Linking, StyleSheet, View, ViewStyle } from "react-native";
6
+ import {
7
+ DeviceEventEmitter,
8
+ Linking,
9
+ StyleSheet,
10
+ View,
11
+ ViewStyle,
12
+ } from "react-native";
7
13
  import * as adadaptedApiRequests from "../api/adadaptedApiRequests";
8
14
  import {
9
15
  Ad,
@@ -15,6 +21,7 @@ import {
15
21
  import { WebView } from "react-native-webview";
16
22
  import { ApiEnv, DeviceOS } from "../index";
17
23
  import { safeInvoke } from "../util";
24
+ import { useEffect, useState } from "react";
18
25
 
19
26
  /**
20
27
  * Props interface for {@link AdZone}.
@@ -58,20 +65,14 @@ interface Props {
58
65
  * @param items - The array of items to "add to list".
59
66
  */
60
67
  onAddToListTriggered?(items: DetailedListItem[]): void;
61
- }
62
-
63
- /**
64
- * State interface for {@link AdZone}.
65
- */
66
- interface State {
67
68
  /**
68
- * Tracks the current ad index being shown.
69
+ * Track the ad zone visibility in parent component.
69
70
  */
70
- adIndexShown: number;
71
+ isAdZoneVisible?: boolean;
71
72
  /**
72
- * Tracks the coordinates when the user started touching the Ad View.
73
+ * Set default ad zone visibility to false.
73
74
  */
74
- touchStartCoords: TouchCoordinates | undefined;
75
+ defaultToInvisibleAdZone?: boolean;
75
76
  }
76
77
 
77
78
  /**
@@ -101,156 +102,144 @@ interface StyleDef {
101
102
  */
102
103
  webView: ViewStyle;
103
104
  }
105
+ /**
106
+ * Timer used for cycling through ads in the zone
107
+ * based on the ad "refresh time" for each ad.
108
+ */
109
+ let cycleAdTimer: ReturnType<typeof setTimeout> | undefined;
104
110
 
105
111
  /**
106
112
  * Creates the AdZone component.
113
+ * @param props - properties passed to AdZone.
114
+ * @returns an AdZone JSX Element.
107
115
  */
108
- export class AdZone extends React.Component<Props, State> {
116
+ export function AdZone(props: Props): JSX.Element {
117
+ // Generates a random number between 0 and (number of available ads - 1).
118
+ const startingAdIndex = Math.floor(
119
+ Math.random() * props.adZoneData.ads.length
120
+ );
121
+
109
122
  /**
110
- * Timer used for cycling through ads in the zone
111
- * based on the ad "refresh time" for each ad.
123
+ * Tracks the current ad index being shown.
112
124
  */
113
- private cycleAdTimer: ReturnType<typeof setTimeout> | undefined;
114
-
125
+ const [adIndexShown, setAdIndexShown] = useState(startingAdIndex);
115
126
  /**
116
- * @inheritDoc
127
+ * Tracks the coordinates when the user started touching the Ad View.
117
128
  */
118
- constructor(props: Props, context?: any) {
119
- super(props, context);
120
-
121
- // Generates a random number between 0 and (number of available ads - 1).
122
- const startingAdIndex = Math.floor(
123
- Math.random() * this.props.adZoneData.ads.length
124
- );
129
+ const [touchStartCoords, setTouchStartCoords] = useState({ x: 0, y: 0 });
130
+ /**
131
+ * Track ad visibility (for off-screen ads).
132
+ */
133
+ const [isAdVisible, setIsAdVisibile] = useState(
134
+ props.defaultToInvisibleAdZone ? false : true
135
+ );
125
136
 
126
- this.state = {
127
- adIndexShown: startingAdIndex,
128
- touchStartCoords: undefined,
137
+ // - Define all useEffect triggers.
138
+ useEffect(() => {
139
+ DeviceEventEmitter.addListener("visibility-event", (event) => {
140
+ setIsAdVisibile(event);
141
+ });
142
+ DeviceEventEmitter.addListener("acknowledge", (itemName) => {
143
+ acknowledge(itemName);
144
+ });
145
+ return () => {
146
+ clearTimeout(cycleAdTimer);
147
+ DeviceEventEmitter.removeAllListeners("visibility-event");
148
+ DeviceEventEmitter.removeAllListeners("acknowledge");
129
149
  };
130
- }
150
+ }, []);
131
151
 
132
- /**
133
- * @inheritDoc
134
- */
135
- public componentDidMount(): void {
136
- this.initializeAd();
137
- }
152
+ useEffect(() => {
153
+ startAdTimer();
154
+ if (isAdVisible) {
155
+ sendAdImpression();
156
+ }
157
+ }, [adIndexShown]);
138
158
 
139
- /**
140
- * @inheritDoc
141
- */
142
- public componentWillUnmount(): void {
143
- if (this.cycleAdTimer) {
144
- clearTimeout(this.cycleAdTimer);
159
+ useEffect(() => {
160
+ if (isAdVisible) {
161
+ sendAdImpression();
145
162
  }
146
- }
163
+ }, [isAdVisible]);
147
164
 
148
165
  /**
149
- * @inheritDoc
166
+ * Generates all component related styles.
167
+ * @returns the styles needed for the component.
150
168
  */
151
- public render(): JSX.Element {
152
- // Generate the styles each render in case the ad is updated with
153
- // new settings that need to be reflected in the styles of the view.
154
- const styles = this.generateStyles();
155
- const currentAd: Ad | undefined =
156
- this.props.adZoneData.ads[this.state.adIndexShown] || undefined;
157
- const finalMainViewStyle = styles.mainView;
158
-
159
- if (!currentAd || !currentAd.creative_url) {
160
- // If there is no ad to display, make the view take up no space.
161
- finalMainViewStyle.width = 0;
162
- finalMainViewStyle.height = 0;
163
- }
164
-
165
- return (
166
- <View style={finalMainViewStyle}>
167
- {currentAd && currentAd.creative_url ? (
168
- <WebView
169
- source={{
170
- uri: currentAd.creative_url,
171
- }}
172
- androidLayerType="hardware"
173
- automaticallyAdjustContentInsets={false}
174
- style={styles.webView}
175
- onTouchStart={(e) => {
176
- this.setState({
177
- touchStartCoords: {
178
- x: e.nativeEvent.pageX,
179
- y: e.nativeEvent.pageY,
180
- },
181
- });
182
- }}
183
- onTouchEnd={(e) => {
184
- if (this.state.touchStartCoords) {
185
- const touchEndCoords: TouchCoordinates = {
186
- x: e.nativeEvent.pageX,
187
- y: e.nativeEvent.pageY,
188
- };
169
+ function generateStyles(): StyleDef {
170
+ return StyleSheet.create({
171
+ mainView: {
172
+ width: "100%",
173
+ height: "100%",
174
+ },
175
+ webView: {
176
+ width: "100%",
177
+ height: "100%",
178
+ },
179
+ });
180
+ }
189
181
 
190
- if (
191
- Math.abs(
192
- this.state.touchStartCoords.x -
193
- touchEndCoords.x
194
- ) < this.props.xyDragDistanceAllowed &&
195
- Math.abs(
196
- this.state.touchStartCoords.y -
197
- touchEndCoords.y
198
- ) < this.props.xyDragDistanceAllowed
199
- ) {
200
- this.onAdZoneSelected(currentAd);
201
- }
182
+ // Generate the styles each render in case the ad is updated with
183
+ // new settings that need to be reflected in the styles of the view.
184
+ const styles = generateStyles();
185
+ const currentAd: Ad | undefined =
186
+ props.adZoneData.ads[adIndexShown] || undefined;
187
+ const finalMainViewStyle = styles.mainView;
202
188
 
203
- // Make sure to reset the start coords
204
- this.setState({
205
- touchStartCoords: undefined,
206
- });
207
- }
208
- }}
209
- />
210
- ) : undefined}
211
- </View>
212
- );
189
+ if (!currentAd || !currentAd.creative_url) {
190
+ // If there is no ad to display, make the view take up no space.
191
+ finalMainViewStyle.width = "0";
192
+ finalMainViewStyle.height = "0";
213
193
  }
214
194
 
215
195
  /**
216
196
  * Triggers when the user selects the ad zone.
217
- * @param currentAd - The ad currently displayed.
197
+ * @param currentlyDisplayedAd - The ad currently displayed.
218
198
  */
219
- private onAdZoneSelected(currentAd: Ad): void {
199
+ function onAdZoneSelected(currentlyDisplayedAd: Ad): void {
220
200
  // Determine the "action type" and perform that specific action.
221
201
  if (
222
- currentAd.action_type === AdActionType.EXTERNAL &&
223
- currentAd.action_path
202
+ currentlyDisplayedAd.action_type === AdActionType.EXTERNAL &&
203
+ currentlyDisplayedAd.action_path
224
204
  ) {
225
205
  // Action Type: EXTERNAL
226
- Linking.openURL(currentAd.action_path).then();
206
+ Linking.openURL(currentlyDisplayedAd.action_path).then();
227
207
  } else if (
228
- currentAd.action_type === AdActionType.CONTENT &&
229
- currentAd.payload &&
230
- currentAd.payload.detailed_list_items
208
+ currentlyDisplayedAd.action_type === AdActionType.CONTENT &&
209
+ currentlyDisplayedAd.payload &&
210
+ currentlyDisplayedAd.payload.detailed_list_items
231
211
  ) {
232
212
  safeInvoke(
233
- this.props.onAddToListTriggered,
234
- currentAd.payload.detailed_list_items
213
+ props.onAddToListTriggered,
214
+ currentlyDisplayedAd.payload.detailed_list_items
235
215
  );
236
216
  }
237
217
 
238
- this.triggerReportAdEvent(currentAd, ReportedEventType.INTERACTION);
239
- if (this.cycleAdTimer) {
240
- clearTimeout(this.cycleAdTimer);
218
+ cycleDisplayedAd();
219
+ }
220
+
221
+ /**
222
+ * Call to acknowledge ATL item(s) added to user list.
223
+ * @param itemName - Detailed list item title from ad that was clicked.
224
+ */
225
+ function acknowledge(itemName: string): void {
226
+ if (props.adZoneData.ads) {
227
+ props.adZoneData.ads.forEach((ad) => {
228
+ ad.payload.detailed_list_items.forEach((item) => {
229
+ if (item.product_title === itemName) {
230
+ triggerReportAdEvent(ad, ReportedEventType.INTERACTION);
231
+ }
232
+ });
233
+ });
241
234
  }
242
- this.cycleDisplayedAd();
243
235
  }
244
236
 
245
237
  /**
246
238
  * Triggered when we need to report an ad event to the API.
247
- * @param currentAd - The ad to send an event for.
239
+ * @param ad - The ad to send an event for.
248
240
  * @param eventType - The event type for the reported event.
249
241
  */
250
- private triggerReportAdEvent(
251
- currentAd: Ad,
252
- eventType: ReportedEventType
253
- ): void {
242
+ function triggerReportAdEvent(ad: Ad, eventType: ReportedEventType): void {
254
243
  // The event timestamp has to be sent as a unix timestamp.
255
244
  const currentTs = Math.round(new Date().getTime() / 1000);
256
245
 
@@ -258,20 +247,20 @@ export class AdZone extends React.Component<Props, State> {
258
247
  adadaptedApiRequests
259
248
  .reportAdEvent(
260
249
  {
261
- app_id: this.props.appId,
262
- session_id: this.props.sessionId,
263
- udid: this.props.udid,
250
+ app_id: props.appId,
251
+ session_id: props.sessionId,
252
+ udid: props.udid,
264
253
  events: [
265
254
  {
266
- ad_id: currentAd.ad_id,
267
- impression_id: currentAd.impression_id,
255
+ ad_id: ad.ad_id,
256
+ impression_id: ad.impression_id,
268
257
  event_type: eventType,
269
258
  created_at: currentTs,
270
259
  },
271
260
  ],
272
261
  },
273
- this.props.deviceOs,
274
- this.props.apiEnv
262
+ props.deviceOs,
263
+ props.apiEnv
275
264
  )
276
265
  .then(() => {
277
266
  // Do nothing with the response for now...
@@ -280,69 +269,97 @@ export class AdZone extends React.Component<Props, State> {
280
269
 
281
270
  /**
282
271
  * Generates a new timer for cycling to the next ad.
283
- * @param timerLength - The length of time(in milliseconds) to initialize
284
- * the timer with.
285
272
  */
286
- private createAdTimer(timerLength: number): void {
287
- if (this.props.adZoneData.ads.length > 0) {
288
- this.cycleAdTimer = setTimeout(() => {
289
- this.cycleDisplayedAd();
290
- }, timerLength);
273
+ function startAdTimer(): void {
274
+ clearTimeout(cycleAdTimer);
275
+
276
+ if (props.adZoneData.ads.length > 0) {
277
+ const refreshTime: number =
278
+ props.adZoneData.ads[adIndexShown].refresh_time * 1000;
279
+ cycleAdTimer = setTimeout(cycleDisplayedAd, refreshTime);
291
280
  }
292
281
  }
293
282
 
294
283
  /**
295
284
  * Cycles to the next ad to display in the current available sequence of ads.
296
285
  */
297
- private cycleDisplayedAd(): void {
286
+ function cycleDisplayedAd(): void {
298
287
  // Start by determining the next ad index to display.
299
288
  let nextAdIndex = 0;
289
+ const lastAd = props.adZoneData.ads[adIndexShown];
300
290
 
301
- if (this.state.adIndexShown < this.props.adZoneData.ads.length - 1) {
302
- nextAdIndex = this.state.adIndexShown + 1;
291
+ if (adIndexShown < props.adZoneData.ads.length - 1) {
292
+ nextAdIndex = adIndexShown + 1;
303
293
  }
304
294
 
305
- this.setState(
306
- {
307
- adIndexShown: nextAdIndex,
308
- },
309
- () => {
310
- this.initializeAd();
311
- }
312
- );
295
+ if (nextAdIndex !== adIndexShown && lastAd.impression_tracked) {
296
+ // Reset ad impression tracking status.
297
+ lastAd.impression_tracked = false;
298
+ } else {
299
+ // Send invisible ad impression if ad was not visible before end of timer cycle.
300
+ triggerReportAdEvent(
301
+ lastAd,
302
+ ReportedEventType.INVISIBLE_IMPRESSION
303
+ );
304
+ }
305
+
306
+ setAdIndexShown(nextAdIndex);
313
307
  }
314
308
 
315
309
  /**
316
- * Performs all ad initialization tasks when a new ad is being displayed.
310
+ * Send ad tracking impression.
317
311
  */
318
- private initializeAd(): void {
319
- // Create the new timer based on the new ad index.
320
- this.createAdTimer(
321
- this.props.adZoneData.ads[this.state.adIndexShown].refresh_time *
322
- 1000
323
- );
312
+ function sendAdImpression(): void {
313
+ const ad = props.adZoneData.ads[adIndexShown];
324
314
 
325
315
  // Trigger an impression event for the ad.
326
- this.triggerReportAdEvent(
327
- this.props.adZoneData.ads[this.state.adIndexShown],
328
- ReportedEventType.IMPRESSION
329
- );
316
+ if (!ad.impression_tracked) {
317
+ triggerReportAdEvent(ad, ReportedEventType.IMPRESSION);
318
+ ad.impression_tracked = true;
319
+ }
330
320
  }
331
321
 
332
- /**
333
- * Generates all component related styles.
334
- * @returns the styles needed for the component.
335
- */
336
- private generateStyles(): StyleDef {
337
- return StyleSheet.create({
338
- mainView: {
339
- width: "100%",
340
- height: "100%",
341
- },
342
- webView: {
343
- width: "100%",
344
- height: "100%",
345
- },
346
- });
347
- }
322
+ // Returned JSX.
323
+ return (
324
+ <View style={finalMainViewStyle}>
325
+ {currentAd && currentAd.creative_url ? (
326
+ <WebView
327
+ source={{
328
+ uri: currentAd.creative_url,
329
+ }}
330
+ androidLayerType="hardware"
331
+ automaticallyAdjustContentInsets={false}
332
+ style={styles.webView}
333
+ onTouchStart={(e) => {
334
+ setTouchStartCoords({
335
+ x: e.nativeEvent.pageX,
336
+ y: e.nativeEvent.pageY,
337
+ });
338
+ }}
339
+ onTouchEnd={(e) => {
340
+ if (touchStartCoords) {
341
+ const touchEndCoords: TouchCoordinates = {
342
+ x: e.nativeEvent.pageX,
343
+ y: e.nativeEvent.pageY,
344
+ };
345
+
346
+ if (
347
+ Math.abs(
348
+ touchStartCoords.x - touchEndCoords.x
349
+ ) < props.xyDragDistanceAllowed &&
350
+ Math.abs(
351
+ touchStartCoords.y - touchEndCoords.y
352
+ ) < props.xyDragDistanceAllowed
353
+ ) {
354
+ onAdZoneSelected(currentAd);
355
+ }
356
+
357
+ // Make sure to reset the start coords
358
+ setTouchStartCoords({ x: 0, y: 0 });
359
+ }
360
+ }}
361
+ />
362
+ ) : undefined}
363
+ </View>
364
+ );
348
365
  }
package/src/index.tsx CHANGED
@@ -4,6 +4,7 @@
4
4
  import * as React from "react";
5
5
  import {
6
6
  AppState,
7
+ DeviceEventEmitter,
7
8
  EmitterSubscription,
8
9
  Linking,
9
10
  NativeModules,
@@ -140,6 +141,10 @@ export interface InitializeProps {
140
141
  * @param payloads - All payloads the client must go through.
141
142
  */
142
143
  onOutOfAppPayloadAvailable?(payloads: OutOfAppDataPayload[]): void;
144
+ /**
145
+ * Change the optional ad zone visibility default setting.
146
+ */
147
+ defaultToInvisibleAdZone?: boolean;
143
148
  }
144
149
 
145
150
  /**
@@ -324,6 +329,14 @@ export class AdadaptedReactNativeSdk {
324
329
  * AppState event listener.
325
330
  */
326
331
  private AppStateOnEventListener: EmitterSubscription | undefined;
332
+ /**
333
+ * Track ad zone visibility for off-screen ads.
334
+ */
335
+ private isAdZoneVisible: boolean = true;
336
+ /**
337
+ * Optional default ad zone visibility for off-screen ads.
338
+ */
339
+ private defaultAdZoneVisibility: boolean | undefined;
327
340
  /**
328
341
  * Gets the Session ID.
329
342
  * @returns the Session ID.
@@ -413,6 +426,10 @@ export class AdadaptedReactNativeSdk {
413
426
  onAddToListTriggered={(items) => {
414
427
  safeInvoke(this.onAddToListTriggered, items);
415
428
  }}
429
+ isAdZoneVisible={!this.onAdZoneVisibilityChanged}
430
+ defaultToInvisibleAdZone={
431
+ this.defaultAdZoneVisibility
432
+ }
416
433
  />
417
434
  ),
418
435
  });
@@ -484,8 +501,6 @@ export class AdadaptedReactNativeSdk {
484
501
  )
485
502
  .then((response) => {
486
503
  this.keywordIntercepts = response.data;
487
-
488
- this.performKeywordSearch("mil");
489
504
  });
490
505
  }
491
506
 
@@ -651,6 +666,23 @@ export class AdadaptedReactNativeSdk {
651
666
  });
652
667
  }
653
668
 
669
+ /**
670
+ * Notify the ad zone of visibility status change for off-screen ads.
671
+ */
672
+ public onAdZoneVisibilityChanged(): void {
673
+ const isVisible = !this.isAdZoneVisible;
674
+ this.isAdZoneVisible = isVisible;
675
+ DeviceEventEmitter.emit("visibility-event", isVisible);
676
+ }
677
+
678
+ /**
679
+ * Notify the adZone to send ad interaction report.
680
+ * @param itemName - Detailed list item title from ad that was clicked.
681
+ */
682
+ public acknowledge(itemName: string): void {
683
+ DeviceEventEmitter.emit("acknowledge", itemName);
684
+ }
685
+
654
686
  /**
655
687
  * Initializes the session for the AdAdapted API and sets up the SDK.
656
688
  * @param props - The props used to initialize the SDK.
@@ -703,6 +735,12 @@ export class AdadaptedReactNativeSdk {
703
735
  this.onOutOfAppPayloadAvailable = props.onOutOfAppPayloadAvailable;
704
736
  }
705
737
 
738
+ // If provided for off-screen ads, set the ad zone visibility for ad tracking.
739
+ if (props.defaultToInvisibleAdZone) {
740
+ this.defaultAdZoneVisibility = props.defaultToInvisibleAdZone;
741
+ this.onAdZoneVisibilityChanged();
742
+ }
743
+
706
744
  return new Promise<void>((resolve, reject) => {
707
745
  this.getDeviceInformation()
708
746
  .then((deviceInfoObj) => {