@media-suite/reactnative-sdk 0.1.1 → 0.2.1

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.
@@ -14,6 +14,7 @@ export function MediaSuiteAd({ spaceCode, style, screenName, onAdLoaded, onError
14
14
  const scrollCheckInterval = useRef(null);
15
15
  const adsRef = useRef([]);
16
16
  const consecutiveFailures = useRef(0);
17
+ const lastClickTime = useRef(0);
17
18
  // Keep adsRef in sync for use inside interval callbacks
18
19
  useEffect(() => { adsRef.current = ads; }, [ads]);
19
20
  // Fetch ads — skip until visitorId is ready to avoid false onError
@@ -125,23 +126,26 @@ export function MediaSuiteAd({ spaceCode, style, screenName, onAdLoaded, onError
125
126
  cancelViewabilityCheck();
126
127
  }
127
128
  }, [ads, index, onError, cancelViewabilityCheck]);
128
- // Click handler — fire-and-forget tracking + open destination URL
129
+ // Click handler — debounced, opens tracking URL via Linking (backend 302 redirects to destination)
129
130
  const handlePress = useCallback(async () => {
131
+ const now = Date.now();
132
+ if (now - lastClickTime.current < 1000)
133
+ return; // 1s debounce
134
+ lastClickTime.current = now;
130
135
  const ad = ads[index];
131
136
  if (!ad)
132
137
  return;
133
138
  const clickUrl = getClickUrl(ad.placement_id, screenName);
134
- // Fire tracking call (don't wait for response)
135
- fetch(clickUrl).catch(() => { });
136
- // Open the destination URL directly
137
- if (ad.click_url) {
138
- await Linking.openURL(ad.click_url).catch(() => { });
139
- }
139
+ await Linking.openURL(clickUrl).catch(() => { });
140
140
  }, [ads, index, getClickUrl, screenName]);
141
141
  if (!ads.length)
142
142
  return null;
143
143
  const ad = ads[index];
144
- return (_jsx(TouchableOpacity, { activeOpacity: 0.9, onPress: handlePress, style: [styles.container, style], accessibilityLabel: ad.alt_text || 'Advertisement', accessibilityRole: "link", children: _jsx(View, { ref: viewRef, collapsable: false, children: _jsx(Image, { source: { uri: ad.image_url }, style: [styles.image, loaded && styles.imageLoaded], resizeMode: "contain", onLoad: () => { setLoaded(true); consecutiveFailures.current = 0; }, onError: handleImageError }, ad.placement_id) }) }));
144
+ return (_jsx(TouchableOpacity, { activeOpacity: 0.9, onPress: handlePress, style: [styles.container, style], accessibilityLabel: ad.alt_text || 'Advertisement', accessibilityRole: "link", children: _jsx(View, { ref: viewRef, collapsable: false, children: _jsx(Image, { source: { uri: ad.image_url }, style: [
145
+ styles.image,
146
+ { aspectRatio: ad.image_width && ad.image_height ? ad.image_width / ad.image_height : 16 / 9 },
147
+ loaded && styles.imageLoaded,
148
+ ], resizeMode: "contain", onLoad: () => { setLoaded(true); consecutiveFailures.current = 0; }, onError: handleImageError }, ad.placement_id) }) }));
145
149
  }
146
150
  const styles = StyleSheet.create({
147
151
  container: {
@@ -150,7 +154,6 @@ const styles = StyleSheet.create({
150
154
  },
151
155
  image: {
152
156
  width: '100%',
153
- aspectRatio: 16 / 9,
154
157
  opacity: 0,
155
158
  },
156
159
  imageLoaded: {
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { createContext, useContext, useEffect, useState, useRef, useCallback } from 'react';
3
- import { useWindowDimensions } from 'react-native';
3
+ import { Platform, useWindowDimensions } from 'react-native';
4
4
  import AsyncStorage from '@react-native-async-storage/async-storage';
5
5
  function uuid() {
6
6
  // Prefer crypto.randomUUID (available since RN 0.73+ with Hermes)
@@ -47,11 +47,20 @@ export function MediaSuiteProvider({ config, children, }) {
47
47
  // Restore or create visitor_id
48
48
  useEffect(() => {
49
49
  (async () => {
50
- let vid = await AsyncStorage.getItem('_ms_vid');
51
- if (!vid) {
50
+ const stored = await AsyncStorage.getItem('_ms_vid');
51
+ let vid;
52
+ if (stored) {
53
+ try {
54
+ vid = JSON.parse(stored);
55
+ }
56
+ catch {
57
+ vid = stored;
58
+ }
59
+ }
60
+ else {
52
61
  vid = uuid();
53
- await AsyncStorage.setItem('_ms_vid', vid);
54
62
  }
63
+ await AsyncStorage.setItem('_ms_vid', JSON.stringify(vid));
55
64
  setVisitorId(vid);
56
65
  })();
57
66
  }, []);
@@ -66,13 +75,15 @@ export function MediaSuiteProvider({ config, children, }) {
66
75
  };
67
76
  }, []);
68
77
  const fetchSpace = useCallback(async (spaceCode) => {
78
+ const CACHE_TTL = 120000; // 120s — matches backend KVS TTL
69
79
  const cacheKey = `${spaceCode}:${device}`;
70
- if (cache.current[cacheKey])
71
- return cache.current[cacheKey];
80
+ const cached = cache.current[cacheKey];
81
+ if (cached && Date.now() - cached.ts < CACHE_TTL)
82
+ return cached.data;
72
83
  if (!visitorId)
73
84
  return null;
74
85
  try {
75
- const url = `${baseUrl}/sdk/serve/${encodeURIComponent(config.companyId)}?spaces=${encodeURIComponent(spaceCode)}&device=${device}&visitor_id=${encodeURIComponent(visitorId)}`;
86
+ const url = `${baseUrl}/sdk/serve/${encodeURIComponent(config.companyId)}?spaces=${encodeURIComponent(spaceCode)}&device=${device}&visitor_id=${encodeURIComponent(visitorId)}&os=${Platform.OS}`;
76
87
  const res = await fetch(url, {
77
88
  headers: { 'api-key': config.apiKey },
78
89
  });
@@ -81,7 +92,7 @@ export function MediaSuiteProvider({ config, children, }) {
81
92
  const json = await res.json();
82
93
  const space = json.data.spaces.find((s) => s.space_code === spaceCode) || null;
83
94
  if (space)
84
- cache.current[cacheKey] = space;
95
+ cache.current[cacheKey] = { data: space, ts: Date.now() };
85
96
  return space;
86
97
  }
87
98
  catch {
@@ -96,7 +107,7 @@ export function MediaSuiteProvider({ config, children, }) {
96
107
  return;
97
108
  pendingImpressions.current = [];
98
109
  const pageUrl = encodeURIComponent(screenName || 'app://unknown');
99
- const url = `${baseUrl}/sdk/impressions?company=${encodeURIComponent(config.companyId)}&placements=${ids.join(',')}&visitor_id=${encodeURIComponent(visitorId)}&device=${device}&page_url=${pageUrl}`;
110
+ const url = `${baseUrl}/sdk/impressions?company=${encodeURIComponent(config.companyId)}&placements=${ids.join(',')}&visitor_id=${encodeURIComponent(visitorId)}&device=${device}&page_url=${pageUrl}&os=${Platform.OS}`;
100
111
  fetch(url, { headers: { 'api-key': config.apiKey } }).catch(() => {
101
112
  if (retryCount < MAX_RETRIES && isMounted.current) {
102
113
  ids.forEach((id) => sentImpressions.current.delete(id));
@@ -119,7 +130,7 @@ export function MediaSuiteProvider({ config, children, }) {
119
130
  }, [flushImpressions]);
120
131
  const getClickUrl = useCallback((placementId, screenName) => {
121
132
  const pageUrl = encodeURIComponent(screenName || 'app://unknown');
122
- return `${baseUrl}/sdk/click/${encodeURIComponent(placementId)}?vid=${encodeURIComponent(visitorId)}&device=${device}&page_url=${pageUrl}`;
133
+ return `${baseUrl}/sdk/click/${encodeURIComponent(placementId)}?vid=${encodeURIComponent(visitorId)}&device=${device}&page_url=${pageUrl}&os=${Platform.OS}`;
123
134
  }, [baseUrl, visitorId, device]);
124
135
  // Render children always — ad components handle the "not ready" state
125
136
  return (_jsx(Ctx.Provider, { value: { config, visitorId, device, fetchSpace, trackImpression, getClickUrl }, children: children }));
package/dist/types.d.ts CHANGED
@@ -9,6 +9,8 @@ export interface MediaSuiteConfig {
9
9
  export interface Ad {
10
10
  placement_id: string;
11
11
  image_url: string;
12
+ image_width?: number;
13
+ image_height?: number;
12
14
  alt_text?: string;
13
15
  click_url?: string;
14
16
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@media-suite/reactnative-sdk",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "MediaSuite Native SDK — React Native SDK for rendering and tracking ads in mobile apps. IAB/MRC viewability standards, impression/click tracking, and ad rotation.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",